mobile app

This commit is contained in:
Julian committed 2026-10-03 01:05:32 +02:00
1 parent 8fbe57b26e
commit e6472e81ed
107 files changed
+6724 -108

No files matched your search

Generated
+3621 -26
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -1,6 +1,8 @@
[workspace]
resolver = "3"
members = ["crates/agent"]
members = ["crates/agent", "app/src-tauri"]
# `cargo build` at the root builds only the agent (the app is built via the Tauri CLI).
default-members = ["crates/agent"]
[workspace.package]
version = "0.1.0"
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+208
View File
@@ -0,0 +1,208 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "app",
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.65.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2",
"svelte": "^5.56.3",
"svelte-check": "^4.6.0",
"typescript": "~6.0.3",
"vite": "^8.0.16",
},
},
},
"packages": {
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@oxc-project/types": ["@oxc-project/types@0.152.0", "", {}, "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.12", "", { "os": "android", "cpu": "arm" }, "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.12", "", { "os": "android", "cpu": "arm64" }, "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.12", "", { "os": "linux", "cpu": "arm" }, "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.12", "", { "os": "win32", "cpu": "x64" }, "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
"@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.3.1", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^1.0.0", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.12.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.12.1", "@tauri-apps/cli-darwin-x64": "2.12.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.1", "@tauri-apps/cli-linux-arm64-gnu": "2.12.1", "@tauri-apps/cli-linux-arm64-musl": "2.12.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.12.1", "@tauri-apps/cli-linux-x64-gnu": "2.12.1", "@tauri-apps/cli-linux-x64-musl": "2.12.1", "@tauri-apps/cli-win32-arm64-msvc": "2.12.1", "@tauri-apps/cli-win32-ia32-msvc": "2.12.1", "@tauri-apps/cli-win32-x64-msvc": "2.12.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-kEDEiGzG+yAc5FeLxtXpES/VN+F2C8H0r4gDVtfRLKxT9np9101d9REG/Kgo2lr0hKi0IpDsODiHnU3+naOmLg=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.12.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8bMnbpJ2jKO/ufrOPL65EMHC3ptw7fzeraoQkwibyfGLqIl+UCWsC0152kmNbW6r9NvVQduXqAZF5mH1QgTWhw=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.12.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-G+2qte8GNqNlEZ9ftd21rbu/APVkl3r/nzNDI7LVxEoqJWVNe9GbEgQUMH1r4PHT20Z2UxR3KjCkltIb0e/Wjg=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.12.1", "", { "os": "linux", "cpu": "arm" }, "sha512-5b3n4DaTW+A1Zntnh4Yn44VyLrlPEpB8kMOBRfWkzNnzolBk+AYhKIfKjCLmoC8Mob3ETodxAEOMkE2YPKEWzQ=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.12.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-APArHhDu+8pXSCkCQmvaz1nD3d8WTmlCoSS/oG475TJ6AATBvyQOAmf5qELgsQ4s2+8MOsop0Zd699RKELF1Cw=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.12.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JaCxcLoV0oGtllVkLr4UAHWG/wTfkEunnUqnsEsGYOp0VRxnUwHg63wFBD0jU2ul5eFfSLIDS4RF2gfzsSo5Jg=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.12.1", "", { "os": "linux", "cpu": "none" }, "sha512-K5+VXM7+SGVDwZleA2+lMgvbTVuA5e1tkiVEawu1Yxj26ZyEfO8h8xTC1rBdRX2xgJDSwCk6vPUBCS6i+Z5slQ=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.12.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Z1QGPr49HJZMktu+Spu6Q1b/f9ANadXvqZZZt2645ua1x1ev6prOgp85eiGv2jRzohFEMLo6aJyaLjLUX5KIvA=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.12.1", "", { "os": "linux", "cpu": "x64" }, "sha512-nHuEhRPOMSJ/8RRhtMRxB9B9lVr7xxpe4DapBz8x1GIau3vdzjHHJ1hAfmw+PyayPLZ6XoaVIHPgI17rX6ii8w=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.12.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-I0oPk2uGh86R8dAs2X5OxSP+/GqSc8jh1Px3l8EqrocSNivsRHDTh2k3PVOqdv163dlV6PIVvmhZAbH/lPfZrA=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.12.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-baNZRBfepAJ5qNFRMwzgBk94xQmkcmTZrNDjZjnzrq2vAaTC3NAhallb/zo3hyoCGQlMS13sdovktrg46NZz6Q=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.12.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.4.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
"obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"rolldown": ["rolldown@1.2.12", "", { "dependencies": { "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.12", "@rolldown/binding-android-arm64": "1.2.12", "@rolldown/binding-darwin-arm64": "1.2.12", "@rolldown/binding-darwin-x64": "1.2.12", "@rolldown/binding-freebsd-x64": "1.2.12", "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", "@rolldown/binding-linux-arm64-gnu": "1.2.12", "@rolldown/binding-linux-arm64-musl": "1.2.12", "@rolldown/binding-linux-ppc64-gnu": "1.2.12", "@rolldown/binding-linux-s390x-gnu": "1.2.12", "@rolldown/binding-linux-x64-gnu": "1.2.12", "@rolldown/binding-linux-x64-musl": "1.2.12", "@rolldown/binding-openharmony-arm64": "1.2.12", "@rolldown/binding-win32-arm64-msvc": "1.2.12", "@rolldown/binding-win32-x64-msvc": "1.2.12" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
"svelte": ["svelte@5.57.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", "@sveltejs/acorn-typescript": "^1.0.13", "@types/estree": "^1.0.9", "acorn": "^8.18.0", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.9.2", "esm-env": "^1.2.1", "esrap": "^2.3.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA=="],
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"vite": ["vite@8.3.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="],
"@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="],
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "framemate-app",
"version": "0.1.0",
"description": "",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri"
},
"license": "MIT",
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.65.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"svelte": "^5.56.3",
"svelte-check": "^4.6.0",
"typescript": "~6.0.3",
"vite": "^8.0.16",
"@tauri-apps/cli": "^2"
}
}
+7
View File
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "framemate-app"
description = "FrameMate companion app"
version.workspace = true
edition.workspace = true
[lib]
# Unique lib name avoids a bin/lib name clash (cargo#8519).
name = "framemate_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["core:default"]
}
+12
View File
@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
+20
View File
@@ -0,0 +1,20 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
keystore.properties
/.tauri
/tauri.settings.gradle
+6
View File
@@ -0,0 +1,6 @@
/src/main/**/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties
@@ -0,0 +1,84 @@
import java.util.Properties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 37
namespace = "dev.framemate.app"
defaultConfig {
// The agent on the Frame speaks plain http/ws on the LAN.
manifestPlaceholders["usesCleartextTraffic"] = "true"
applicationId = "dev.framemate.app"
minSdk = 24
targetSdk = 37
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
optimization {
enable = true
}
proguardFiles(
*fileTree(".") {
include("**/*.pro")
exclude("build/**")
}.files.toTypedArray()
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures {
buildConfig = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = file("tauri.build.gradle.kts"))
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.framemate_app"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,43 @@
package dev.framemate.app
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}
override fun onWebViewCreate(webView: WebView) {
super.onWebViewCreate(webView)
webView.addJavascriptInterface(FullscreenBridge(), "FrameMateAndroid")
}
/**
* The app is portrait-only (see AndroidManifest). The headset view calls
* `FrameMateAndroid.setFullscreen(true)` to go landscape without system bars.
* WebViews can't do this themselves: the Screen Orientation API is browser-only.
*/
inner class FullscreenBridge {
@JavascriptInterface
fun setFullscreen(enabled: Boolean) = runOnUiThread {
requestedOrientation =
if (enabled) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
val bars = WindowCompat.getInsetsController(window, window.decorView)
if (enabled) {
bars.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
bars.hide(WindowInsetsCompat.Type.systemBars())
} else {
bars.show(WindowInsetsCompat.Type.systemBars())
}
}
}
}
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.framemate_app" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>
@@ -0,0 +1,4 @@
<resources>
<string name="app_name">"FrameMate"</string>
<string name="main_activity_title">"FrameMate"</string>
</resources>
@@ -0,0 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.framemate_app" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,22 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:9.3.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}
@@ -0,0 +1,23 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:9.3.1")
}
@@ -0,0 +1,75 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import javax.inject.Inject
import org.gradle.process.ExecOperations
abstract class BuildTask : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@Input
var rootDirRel: String? = null
@Input
var projectDir: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """bun""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
execOperations.exec {
workingDir(File(projectDir, rootDirRel))
executable(executable)
args(args)
if (logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}
@@ -0,0 +1,86 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.register(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
) {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
projectDir = project.projectDir.path
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}
@@ -0,0 +1,26 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Our old plugin template had a bug that caused missing `consumer-rules.pro` files
# so we have to disable it for now.
# See https://github.com/tauri-apps/tauri/pull/15828#issuecomment-5199937796
android.proguard.failOnMissingFiles=false
# Keeping Gradle 9 compatible with current plugins
# TODO: Remove this in v3
android.builtInKotlin=false
android.newDsl=false
Binary file not shown.
@@ -0,0 +1,6 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+82
View File
@@ -0,0 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
@@ -0,0 +1,3 @@
include ':app'
apply from: 'tauri.settings.gradle'
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Adaptive-icon foreground for Android, generated from assets/framemate-black.svg without
its black tile (the background layer is black). Content is scaled to 0.6 around the centre
so it stays inside the launcher mask's safe zone (inner 66 of 108 dp), also for round masks.
Render with rsvg-convert into gen/android/.../mipmap-*/ic_launcher_foreground.png. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g transform="translate(102.4 102.4) scale(0.6)">
<!-- Frame -->
<path
d="M 79.770658,81.497471 C 88.802511,66.421772 440,72 440,72 v 72 c 0,0 -287.53399,-8.46601 -296,0 -8.46601,8.46601 0,296 0,296 H 72 c 0,0 -1.261196,-343.426831 7.770658,-358.502529 z"
fill="#000000"
id="path1"
style="fill:#ffffff;fill-opacity:1" />
<!-- Blue companion shape -->
<path
d="m 208,440 c 0,-128.13006 103.86994,-232 232,-232 0,0 3.28372,219.57202 -3.45363,230.27319 C 429.80902,448.97435 208,440 208,440 Z"
fill="#2196f3"
id="path2"
/>
<!-- Friendly face -->
<circle
cx="180.0067"
cy="185.06584"
r="23.654644"
fill="#000000"
id="circle2"
style="stroke-width:1.81959;fill:#ffffff;fill-opacity:1" />
<circle
cx="322.91068"
cy="185.06584"
r="23.654644"
fill="#000000"
id="circle3"
style="stroke-width:1.81959;fill:#ffffff;fill-opacity:1" />
<path
d="m 217.29542,224.1911 c 14.60088,23.48211 53.53662,23.48211 68.13747,0"
fill="none"
stroke="#000000"
stroke-width="10.7716"
stroke-linecap="round"
id="path3"
style="stroke-width:21.3045;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+6
View File
@@ -0,0 +1,6 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
framemate_app_lib::run()
}
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "FrameMate",
"version": "0.1.0",
"identifier": "dev.framemate.app",
"build": {
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "bun run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "FrameMate",
"width": 412,
"height": 892
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+74
View File
@@ -0,0 +1,74 @@
/* Colours follow the Steam mobile app / SteamOS: dark blue-grey surfaces, #1a9fff accent. */
:root {
--bg: #171d25;
--topbar: #171a21;
--surface: #1e252e;
--surface-2: #2a3542;
--divider: #2a313b;
--track: #3d4450;
--text: #dcdedf;
--muted: #8b929a;
--accent: #1a9fff;
--accent-2: #00bbff;
--green: #59bf40;
--yellow: #e5b029;
--red: #e0423c;
--gutter: 16px;
color-scheme: dark;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 15px;
line-height: 1.4;
color: var(--text);
background: var(--bg);
-webkit-tap-highlight-color: transparent;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
overscroll-behavior: none;
}
a {
color: inherit;
text-decoration: none;
}
button,
input {
font: inherit;
color: inherit;
}
.muted {
color: var(--muted);
}
/* Steam's blue call-to-action button ("EXPLORE YOUR QUEUE"). */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 44px;
padding: 0 20px;
border: 0;
border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
color: #fff;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
}
.empty {
padding: 16px var(--gutter);
background: var(--surface);
color: var(--muted);
}
+16
View File
@@ -0,0 +1,16 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
declare global {
namespace App {
interface PageState {
/** Headset view in fullscreen (shallow route, so Back leaves fullscreen). */
fullscreen?: boolean;
}
}
interface Window {
/** Native bridge from MainActivity.kt (Android only). */
FrameMateAndroid?: { setFullscreen(enabled: boolean): void };
}
}
export {};
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#171a21" />
<title>FrameMate</title>
%sveltekit.head%
</head>
<body>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
// Connection to the framemate-agent on the Frame. Holds the latest state pushed over
// `/api/ws` and reconnects on its own. Settings live in localStorage.
import type { AgentState } from "./types";
const STORAGE_KEY = "framemate.connection";
const DEFAULT_PORT = 7380;
const RETRY_MS = 3000;
export interface ConnectionSettings {
/** Hostname or IP, optionally with `:port`. */
host: string;
token: string;
}
export type ConnectionStatus = "unconfigured" | "connecting" | "connected" | "offline";
function loadSettings(): ConnectionSettings {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "null");
if (saved?.host !== undefined && saved?.token !== undefined) return saved;
} catch {}
return { host: "frame.local", token: "" };
}
class Agent {
settings = $state<ConnectionSettings>(loadSettings());
state = $state<AgentState | null>(null);
status = $state<ConnectionStatus>("unconfigured");
#socket: WebSocket | null = null;
#retry: ReturnType<typeof setTimeout> | undefined;
get configured() {
return this.settings.host.trim() !== "" && this.settings.token.trim() !== "";
}
/** `host:port` of the agent. */
get authority() {
const host = this.settings.host.trim();
return /:\d+$/.test(host) ? host : `${host}:${DEFAULT_PORT}`;
}
/** WebSocket URL for an agent endpoint, with the token attached. */
socketUrl(path: string) {
return `ws://${this.authority}${path}?token=${encodeURIComponent(this.settings.token.trim())}`;
}
save(settings: ConnectionSettings) {
this.settings = { host: settings.host.trim(), token: settings.token.trim() };
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.settings));
} catch {}
this.state = null;
this.connect();
}
connect() {
this.#close();
if (!this.configured) {
this.status = "unconfigured";
return;
}
this.status = "connecting";
const socket = new WebSocket(this.socketUrl("/api/ws"));
this.#socket = socket;
socket.onmessage = event => {
this.state = JSON.parse(event.data);
this.status = "connected";
};
socket.onclose = () => {
if (this.#socket !== socket) return; // replaced by a newer connection
this.#socket = null;
this.status = "offline";
this.#retry = setTimeout(() => this.connect(), RETRY_MS);
};
}
#close() {
clearTimeout(this.#retry);
const socket = this.#socket;
this.#socket = null;
socket?.close();
}
}
export const agent = new Agent();
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
// Steam's battery glyph (same geometry as the client's), with the fill drawn to scale.
let { level, charging = false, size = 20 }: { level: number | null | undefined; charging?: boolean; size?: number } =
$props();
const fill = $derived(Math.max(0, Math.min(1, level ?? 0)));
const color = $derived(charging || fill > 0.2 ? "var(--green)" : fill > 0.1 ? "var(--yellow)" : "var(--red)");
</script>
<svg viewBox="-4 0 52 36" width={(size * 52) / 36} height={size} aria-hidden="true">
<path fill-rule="evenodd" d="M39 6H0V30H39V22H42V14H39V6ZM36 9H3V27H36V9Z" fill="currentColor" />
{#if level != null}
<rect x="6" y="12" width={27 * fill} height="12" fill={color} />
{/if}
{#if charging}
<path d="M16 20L21 11V16H26L21 25V20H16Z" fill="white" />
{/if}
</svg>
+46
View File
@@ -0,0 +1,46 @@
<script lang="ts">
// Steam CDN artwork; tools and runtimes have none, so fall back to a neutral tile.
import { appArt } from "$lib/format";
import Icon from "./Icon.svelte";
let { appid, kind = "header" }: { appid: number; kind?: "header" | "capsule" } = $props();
let failed = $state(false);
$effect(() => {
appid;
failed = false;
});
</script>
<div class="art {kind}">
{#if failed}
<Icon name="library" size={kind === "header" ? 40 : 22} />
{:else}
<img src={appArt(appid, kind)} alt="" loading="lazy" onerror={() => (failed = true)} />
{/if}
</div>
<style>
.art {
display: grid;
place-items: center;
background: var(--surface-2);
color: var(--muted);
overflow: hidden;
flex: none;
}
.header {
width: 100%;
aspect-ratio: 460 / 215;
}
.capsule {
width: 92px;
aspect-ratio: 184 / 69;
border-radius: 2px;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
</style>
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts" module>
// Icons are SVGs collected from the Steam client UI (they use currentColor).
const icons = import.meta.glob("../icons/*.svg", { query: "?raw", import: "default", eager: true }) as Record<string, string>;
</script>
<script lang="ts">
let { name, size = 24 }: { name: string; size?: number } = $props();
const svg = $derived(icons[`../icons/${name}.svg`] ?? "");
</script>
<span class="icon" style:width="{size}px" style:height="{size}px" aria-hidden="true">{@html svg}</span>
<style>
.icon {
display: inline-flex;
flex: none;
}
.icon :global(svg) {
width: 100%;
height: 100%;
}
</style>
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
let { value, paused = false }: { value: number | null | undefined; paused?: boolean } = $props();
</script>
<div class="track" role="progressbar" aria-valuenow={value ?? 0} aria-valuemin="0" aria-valuemax="100">
<div class="bar" class:paused style:width="{Math.max(0, Math.min(100, value ?? 0))}%"></div>
</div>
<style>
.track {
height: 6px;
background: var(--track);
border-radius: 3px;
overflow: hidden;
}
.bar {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
transition: width 0.4s ease;
}
.paused {
background: var(--muted);
}
</style>
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts">
// SteamOS-settings style row: label on the left, value on the right.
import type { Snippet } from "svelte";
let { label, value, children }: { label: string; value?: unknown; children?: Snippet } = $props();
</script>
<div class="row">
<span class="label">{label}</span>
<span class="value">
{#if children}{@render children()}{:else}{value ?? "–"}{/if}
</span>
</div>
<style>
.row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
min-height: 48px;
padding: 0 var(--gutter);
background: var(--surface);
border-bottom: 1px solid var(--divider);
}
.label {
color: var(--text);
}
.value {
color: var(--muted);
text-align: right;
overflow-wrap: anywhere;
}
</style>
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
import type { Snippet } from "svelte";
let { title, aside, children }: { title: string; aside?: Snippet; children: Snippet } = $props();
</script>
<section>
<header>
<h2>{title}</h2>
{@render aside?.()}
</header>
{@render children()}
</section>
<style>
section {
margin: 20px 0 0;
}
header {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 0 var(--gutter) 8px;
}
h2 {
margin: 0;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text);
}
</style>
+40
View File
@@ -0,0 +1,40 @@
export function percent(fraction: number | null | undefined): string {
return fraction == null ? "–" : `${Math.round(fraction * 100)}%`;
}
export function bytes(n: number | null | undefined): string {
if (n == null) return "–";
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
while (n >= 1000 && i < units.length - 1) {
n /= 1000;
i++;
}
return `${n.toFixed(i && n < 100 ? 1 : 0)} ${units[i]}`;
}
export function duration(seconds: number | null | undefined): string {
if (seconds == null || seconds < 0) return "–";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (h) return `${h}h ${m}m`;
if (m) return `${m}m`;
return `${Math.floor(seconds)}s`;
}
export function since(ms: number | null | undefined): string {
return ms ? duration((Date.now() - ms) / 1000) : "–";
}
/** Steam CDN artwork for an app; tools and runtimes have none (handle onerror). */
export function appArt(appid: number, kind: "header" | "capsule" = "header"): string {
const file = kind === "header" ? "header.jpg" : "capsule_184x69.jpg";
return `https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/${file}`;
}
/** OpenVR EDeviceActivityLevel. */
export function hmdActivity(level: number | undefined): string {
return (
{ [-1]: "Unknown", 0: "Idle", 1: "In use", 2: "In use", 3: "Standby", 4: "Idle" } as Record<number, string>
)[level ?? -1] ?? "Unknown";
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none" class="_1CpOAgPPD7f_fGI4HaYX6C _3RbjUi5Vop-Jk9vriR_zq4"><path d="M17.54 9.53998L11.81 15.27L33.77 15.27V20.27H11.81L17.54 26L14 29.54L2.23002 17.77L14 5.99998L17.54 9.53998Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 280 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M21.1862 2.52116C20.1687 2.17914 19.0936 2 18 2C15.3478 2 12.8043 3.05357 10.9289 4.92893C9.05357 6.8043 8 9.34784 8 12V19L4 24V26H32V24L28 19V14.4025C23.9218 12.9611 21 9.07177 21 4.5C21 3.82354 21.064 3.16202 21.1862 2.52116ZM21.46 32.897C20.4483 33.6133 19.2396 33.9986 18 34C16.7604 33.9986 15.5517 33.6133 14.54 32.897C13.5282 32.1807 12.7632 31.1687 12.35 30H23.65C23.2368 31.1687 22.4718 32.1807 21.46 32.897Z"></path><path fill="#1A9FFF" d="M36 4.5C36 6.98528 33.9853 9 31.5 9C29.0147 9 27 6.98528 27 4.5C27 2.01472 29.0147 0 31.5 0C33.9853 0 36 2.01472 36 4.5Z"></path></svg>

After

Width:  |  Height:  |  Size: 725 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path d="M34 6V30H17V26H30V10H6V15H2V6H34ZM2 18V22C4.12173 22 6.15656 22.8429 7.65685 24.3431C9.15715 25.8434 10 27.8783 10 30H14C14 26.8174 12.7357 23.7652 10.4853 21.5147C8.23484 19.2643 5.1826 18 2 18ZM2 25V30H7C7 29.3434 6.87067 28.6932 6.6194 28.0866C6.36812 27.48 5.99983 26.9288 5.53553 26.4645C5.07124 26.0002 4.52005 25.6319 3.91342 25.3806C3.30679 25.1293 2.65661 25 2 25Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 489 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 36 36"><path fill="currentColor" d="M31.5,9.6c-0.3-0.3-0.7-0.6-1.1-1V8.4c0,0,0-0.6-0.6-1.1s-3.9-1.7-5.1-1.7c-0.7,0-0.9,0.2-1.2,0.4c-0.2,0.1-0.3,0.2-0.5,0.2 H12.9c-0.2,0-0.4-0.1-0.5-0.2c-0.2-0.2-0.5-0.4-1.2-0.4c-1.1,0-4.5,1.1-5.1,1.7S5.6,8.4,5.6,8.4v0.1c-0.4,0.3-0.8,0.7-1.1,1 C3.4,10.7,0,20.2,0,25.3s3.4,5.6,3.4,5.6c0.9,0,2.3-1.8,3.7-3.5c1.2-1.5,2.3-3,3.1-3.2c1.7-0.6,14.1-0.6,15.8,0 c0.8,0.3,1.9,1.7,3.1,3.2c1.4,1.7,2.8,3.5,3.7,3.5c0,0,3.4-0.6,3.4-5.6S32.6,10.7,31.5,9.6z M8.4,14.6c-1.2,0-2.2-1-2.2-2.2 s1-2.2,2.2-2.2s2.2,1,2.2,2.2S9.7,14.6,8.4,14.6z M15.8,18.8c0,0.3-0.3,0.6-0.6,0.6h-0.8v0.8c0,0.3-0.3,0.6-0.6,0.6h-1.1 c-0.3,0-0.6-0.3-0.6-0.6v-0.8h-0.8c-0.3,0-0.6-0.3-0.6-0.6v-1.1c0-0.3,0.3-0.6,0.6-0.6h0.8v-0.8c0-0.3,0.3-0.6,0.6-0.6h1.1 c0.3,0,0.6,0.3,0.6,0.6v0.8h0.8c0.3,0,0.6,0.3,0.6,0.6V18.8z M27.6,8.7c0.8,0,1.4,0.6,1.4,1.4s-0.6,1.4-1.4,1.4s-1.4-0.6-1.4-1.4 S26.8,8.7,27.6,8.7z M23.1,20.2c-1.2,0-2.2-1-2.2-2.2s1-2.2,2.2-2.2s2.2,1,2.2,2.2S24.3,20.2,23.1,20.2z M25,14.1 c-0.8,0-1.4-0.6-1.4-1.4c0-0.8,0.6-1.4,1.4-1.4s1.4,0.6,1.4,1.4C26.4,13.4,25.8,14.1,25,14.1z M27.6,16.6c-0.8,0-1.4-0.6-1.4-1.4 s0.6-1.4,1.4-1.4s1.4,0.6,1.4,1.4S28.3,16.6,27.6,16.6z M30.1,14.1c-0.8,0-1.4-0.6-1.4-1.4c0-0.8,0.6-1.4,1.4-1.4s1.4,0.6,1.4,1.4 C31.5,13.4,30.9,14.1,30.1,14.1z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M29 23V27H7V23H2V32H34V23H29Z" fill="currentColor"></path><svg x="0" y="0" width="32" height="25"><path class="DownloadArrow" d="M20 14.1716L24.5858 9.58578L27.4142 12.4142L18 21.8284L8.58582 12.4142L11.4142 9.58578L16 14.1715V2H20V14.1716Z" fill="currentColor"></path></svg></svg>

After

Width:  |  Height:  |  Size: 402 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path d="M18 26V31H2V26C2 23.8783 2.84285 21.8434 4.34315 20.3431C5.84344 18.8429 7.87827 18 10 18C12.1217 18 14.1566 18.8429 15.6569 20.3431C17.1571 21.8434 18 23.8783 18 26ZM10 15C10.89 15 11.76 14.7361 12.5001 14.2416C13.2401 13.7471 13.8169 13.0443 14.1575 12.2221C14.4981 11.3998 14.5872 10.495 14.4135 9.6221C14.2399 8.74918 13.8113 7.94736 13.182 7.31802C12.5526 6.68868 11.7508 6.2601 10.8779 6.08647C10.005 5.91283 9.10019 6.00195 8.27792 6.34254C7.45566 6.68314 6.75285 7.25991 6.25839 7.99994C5.76392 8.73996 5.5 9.60999 5.5 10.5C5.49868 11.0913 5.61418 11.6771 5.83986 12.2236C6.06554 12.7702 6.39695 13.2668 6.81508 13.6849C7.23321 14.103 7.72981 14.4345 8.27637 14.6601C8.82293 14.8858 9.40868 15.0013 10 15ZM31.66 18.34C30.8643 17.5434 29.9094 16.9238 28.8578 16.5216C27.8062 16.1194 26.6815 15.9437 25.5574 16.006C24.4332 16.0683 23.3348 16.3672 22.3341 16.8831C21.3334 17.399 20.4528 18.1204 19.75 19C21.2201 21.0373 22.0077 23.4877 22 26V29H34V24C34.0008 22.9491 33.7946 21.9084 33.3931 20.9372C32.9916 19.966 32.4027 19.0835 31.66 18.34ZM26 13C26.89 13 27.76 12.7361 28.5001 12.2416C29.2401 11.7471 29.8169 11.0443 30.1575 10.2221C30.4981 9.39981 30.5872 8.49501 30.4135 7.6221C30.2399 6.74918 29.8113 5.94736 29.182 5.31802C28.5526 4.68868 27.7508 4.2601 26.8779 4.08647C26.005 3.91283 25.1002 4.00195 24.2779 4.34254C23.4557 4.68314 22.7529 5.25991 22.2584 5.99994C21.7639 6.73996 21.5 7.60999 21.5 8.5C21.4987 9.09132 21.6142 9.67708 21.8399 10.2236C22.0655 10.7702 22.397 11.2668 22.8151 11.6849C23.2332 12.103 23.7298 12.4345 24.2764 12.6601C24.8229 12.8858 25.4087 13.0013 26 13Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path d="M33 13.3C33.0114 12.2663 32.7021 11.2545 32.1147 10.4038C31.5273 9.5532 30.6907 8.90553 29.72 8.54998C25.9132 7.45272 21.9604 6.94682 18 7.04998C14.0396 6.94682 10.0868 7.45272 6.28 8.54998C5.30932 8.90553 4.4727 9.5532 3.88531 10.4038C3.29791 11.2545 2.98862 12.2663 3 13.3L0 14.05V20.05L3 20.81C3.08508 22.0931 3.51701 23.3289 4.24968 24.3857C4.98235 25.4424 5.98826 26.2803 7.16 26.81L10.16 28.08C10.7695 28.3298 11.4453 28.3671 12.0785 28.1856C12.7117 28.0042 13.2654 27.6147 13.65 27.08C14.1242 26.3803 14.779 25.8222 15.5449 25.4648C16.3108 25.1074 17.1592 24.964 18 25.05C18.8449 24.9722 19.6951 25.1256 20.4595 25.4937C21.224 25.8619 21.874 26.4309 22.34 27.14C22.7246 27.6747 23.2782 28.0642 23.9115 28.2456C24.5447 28.4271 25.2205 28.3898 25.83 28.14L28.83 26.87C30.0017 26.3403 31.0076 25.5024 31.7403 24.4457C32.473 23.3889 32.9049 22.1531 32.99 20.87L35.99 20.11V14.11L33 13.3ZM22.16 13C20.9452 12.7243 19.7054 12.5735 18.46 12.55C16.9073 12.5083 15.3555 12.6595 13.84 13L13.16 10.08C14.925 9.68481 16.7318 9.50682 18.54 9.54998C19.988 9.57538 21.4292 9.75302 22.84 10.08L22.16 13Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M32 15V32H22V22H14V32H4V15L18 3L32 15Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 194 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path d="M16 4H4V16H16V4Z" fill="currentColor"></path><path d="M16 20H4V32H16V20Z" fill="currentColor"></path><path d="M32 4H20V16H32V4Z" fill="currentColor"></path><path d="M32 20H20V32H32V20Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 300 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"><path fill="currentColor" d="M10.9691 29.9823L14.2224 17.7778H7.11133L21.3336 1.77783L18.6669 13.3334H25.778L10.9691 29.9823Z"></path></svg>

After

Width:  |  Height:  |  Size: 212 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path d="M33 20.38V15.62L29.07 14.9C28.8121 14.015 28.453 13.1628 28 12.36L30.27 9.08L26.92 5.71L23.64 8C22.8372 7.54696 21.985 7.18793 21.1 6.93L20.38 3H15.62L14.9 6.93C14.015 7.18793 13.1628 7.54696 12.36 8L9.08 5.71L5.71 9.08L8 12.36C7.54696 13.1628 7.18793 14.015 6.93 14.9L3 15.62V20.38L6.93 21.1C7.18793 21.985 7.54696 22.8372 8 23.64L5.71 26.92L9.08 30.29L12.36 28C13.1637 28.4461 14.0159 28.7984 14.9 29.05L15.62 33H20.38L21.1 29.07C21.985 28.8121 22.8372 28.453 23.64 28L26.92 30.27L30.29 26.9L28 23.64C28.4461 22.8363 28.7984 21.9841 29.05 21.1L33 20.38ZM18 23C17.0111 23 16.0444 22.7068 15.2221 22.1573C14.3999 21.6079 13.759 20.827 13.3806 19.9134C13.0022 18.9998 12.9031 17.9945 13.0961 17.0245C13.289 16.0546 13.7652 15.1637 14.4645 14.4645C15.1637 13.7652 16.0546 13.289 17.0245 13.0961C17.9945 12.9031 18.9998 13.0022 19.9134 13.3806C20.827 13.759 21.6079 14.3999 22.1573 15.2221C22.7068 16.0444 23 17.0111 23 18C23 18.6566 22.8707 19.3068 22.6194 19.9134C22.3681 20.52 21.9998 21.0712 21.5355 21.5355C21.0712 21.9998 20.52 22.3681 19.9134 22.6194C19.3068 22.8707 18.6566 23 18 23Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M33 19.9286H3L7.35714 6H28.6429L33 19.9286ZM3 31.7143V24.2143H33V31.7143H3ZM21.0952 26.1826C21.4476 25.9471 21.8619 25.8214 22.2857 25.8214C22.854 25.8214 23.3991 26.0472 23.8009 26.4491C24.2028 26.8509 24.4286 27.396 24.4286 27.9643C24.4286 28.3881 24.3029 28.8024 24.0674 29.1548C23.832 29.5072 23.4973 29.7818 23.1058 29.944C22.7142 30.1062 22.2833 30.1486 21.8677 30.066C21.452 29.9833 21.0702 29.7792 20.7705 29.4795C20.4708 29.1798 20.2667 28.798 20.184 28.3823C20.1013 27.9667 20.1438 27.5358 20.306 27.1442C20.4682 26.7527 20.7428 26.418 21.0952 26.1826ZM28.7143 25.8214C28.2905 25.8214 27.8762 25.9471 27.5238 26.1826C27.1714 26.418 26.8967 26.7527 26.7345 27.1442C26.5724 27.5358 26.5299 27.9667 26.6126 28.3823C26.6953 28.798 26.8994 29.1798 27.1991 29.4795C27.4987 29.7792 27.8806 29.9833 28.2962 30.066C28.7119 30.1486 29.1428 30.1062 29.5343 29.944C29.9259 29.7818 30.2605 29.5072 30.496 29.1548C30.7315 28.8024 30.8571 28.3881 30.8571 27.9643C30.8571 27.396 30.6314 26.8509 30.2295 26.4491C29.8277 26.0472 29.2826 25.8214 28.7143 25.8214Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+117
View File
@@ -0,0 +1,117 @@
// Shape of the agent's `/api/ws` / `/api/state` JSON (see crates/agent/src/hub.rs and
// shim.js). Steam topics are loosely typed on the agent side; every field may be
// missing when Steam isn't connected or an API changed, hence the many optionals.
export interface AgentState {
agent: { version: string; hostname: string; started_at_ms: number; updated_at_ms: number };
steam: {
connected: boolean;
error: string | null;
last_event_ms: number | null;
topics: SteamTopics;
topic_errors: Record<string, string>;
};
power: { battery: KernelBattery | null; external_power: boolean | null } | null;
steamos: { available: boolean; error: string | null; properties: Record<string, unknown> };
stream: StreamStats | null;
}
export interface SteamTopics {
battery?: {
has_battery: boolean;
level: number;
seconds_remaining: number;
/** 1 = on battery, 2 = on AC */
ac_state: number;
battery_state: number;
};
vr_devices?: VrDevice[];
vr_state?: { hmd_activity?: number; scene_appid?: number | null; scene_app_name?: string | null };
running_apps?: { appid: number; name: string | null }[];
last_app_event?: { appid: number; name: string | null; running: boolean; at_ms: number };
downloads?: DownloadItem[];
download_overview?: DownloadOverview;
network?: {
connected: boolean | null;
internet: boolean | null;
steam: boolean | null;
wifi_enabled: boolean | null;
interfaces: { name: string; rx_bytes_per_sec: number; tx_bytes_per_sec: number }[];
};
perf?: {
battery_temp_c: number | null;
tdp_limit_w: number | null;
fps_limit: number | null;
low_latency_mode: boolean | null;
};
system_info?: {
os: string;
os_version: string;
os_variant: string;
os_build: string;
kernel: string;
hostname: string;
steam_version: number;
steam_build_date: string;
gpu: string;
gpu_driver: string;
cpu_cores: number;
ram_mb: number;
};
user?: { account_name: string | null; persona_name: string | null; persona_state: number | null };
}
export interface VrDevice {
path: string;
model: string | null;
serial: string | null;
/** 1 = HMD, 2 = controller */
device_class: number | null;
/** 0..1 */
battery: number | null;
charging: boolean | null;
connected: boolean | null;
}
export interface DownloadItem {
appid: number;
name: string | null;
queue_index: number;
active: boolean;
paused: boolean;
completed: boolean;
completed_time: number;
percent: number | null;
error: string | null;
}
export interface DownloadOverview {
appid: number | null;
name?: string | null;
state?: string | null;
paused?: boolean;
percent?: number;
bytes_per_sec?: number;
eta_sec?: number;
is_workshop?: boolean;
is_shader?: boolean;
}
export interface KernelBattery {
status: string | null;
health: string | null;
capacity_percent: number | null;
power_w: number | null;
temp_c: number | null;
cycle_count: number | null;
}
export interface StreamStats {
viewers: number;
width: number;
height: number;
source_fps: number;
fps: number;
kbit_per_sec: number;
encode_ms: number;
}
+126
View File
@@ -0,0 +1,126 @@
<script lang="ts">
import "../app.css";
import { onMount, type Snippet } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { agent } from "$lib/agent.svelte";
import Battery from "$lib/components/Battery.svelte";
import Icon from "$lib/components/Icon.svelte";
let { children }: { children: Snippet } = $props();
const tabs = [
{ href: "/", icon: "home", label: "Home" },
{ href: "/downloads", icon: "download", label: "Downloads" },
{ href: "/view", icon: "cast", label: "View" },
{ href: "/system", icon: "performance", label: "System" },
{ href: "/settings", icon: "settings", label: "Settings" },
];
const battery = $derived(agent.state?.steam.topics.battery);
onMount(() => {
agent.connect();
if (!agent.configured) goto("/settings");
// Mobile WebViews drop sockets in the background; reconnect when we come back.
const onVisible = () => {
if (document.visibilityState === "visible" && agent.status !== "connected") agent.connect();
};
document.addEventListener("visibilitychange", onVisible);
return () => document.removeEventListener("visibilitychange", onVisible);
});
</script>
<div class="app">
<header class="topbar">
<img src="/favicon.svg" alt="" class="logo" />
<span class="title">FrameMate</span>
<span class="spacer"></span>
{#if battery}
<span class="battery">
{Math.round(battery.level * 100)}%
<Battery level={battery.level} charging={battery.ac_state === 2} size={18} />
</span>
{/if}
<span class="status {agent.status}" title={agent.status}></span>
</header>
<main>
{@render children()}
</main>
<nav class="tabs">
{#each tabs as tab}
<a href={tab.href} class:active={page.url.pathname === tab.href} aria-label={tab.label}>
<Icon name={tab.icon} size={26} />
</a>
{/each}
</nav>
</div>
<style>
.app {
display: flex;
flex-direction: column;
height: 100dvh;
}
.topbar {
display: flex;
align-items: center;
gap: 10px;
padding: calc(env(safe-area-inset-top) + 10px) var(--gutter) 10px;
background: var(--topbar);
}
.logo {
width: 30px;
height: 30px;
}
.title {
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.spacer {
flex: 1;
}
.battery {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
}
.status {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.status.connected {
background: var(--green);
}
.status.offline {
background: var(--red);
}
main {
flex: 1;
overflow-y: auto;
padding-bottom: 24px;
}
.tabs {
display: flex;
background: var(--topbar);
padding-bottom: env(safe-area-inset-bottom);
}
.tabs a {
flex: 1;
display: flex;
justify-content: center;
padding: 12px 0;
color: var(--text);
border-top: 2px solid transparent;
}
.tabs a.active {
color: var(--accent);
border-top-color: var(--accent);
}
</style>
+5
View File
@@ -0,0 +1,5 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we use adapter-static with a fallback to index.html to put the site in SPA mode
// See: https://svelte.dev/docs/kit/single-page-apps
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const ssr = false;
+176
View File
@@ -0,0 +1,176 @@
<script lang="ts">
import { agent } from "$lib/agent.svelte";
import Battery from "$lib/components/Battery.svelte";
import GameArt from "$lib/components/GameArt.svelte";
import Icon from "$lib/components/Icon.svelte";
import Progress from "$lib/components/Progress.svelte";
import Section from "$lib/components/Section.svelte";
import { bytes, duration, hmdActivity, percent } from "$lib/format";
const topics = $derived(agent.state?.steam.topics);
const battery = $derived(topics?.battery);
const charging = $derived(battery?.ac_state === 2);
const devices = $derived(topics?.vr_devices ?? []);
const controllers = $derived(
devices
.filter(d => d.device_class === 2)
.sort((a, b) => (a.model ?? "").localeCompare(b.model ?? "")),
);
const running = $derived(topics?.running_apps ?? []);
const download = $derived(topics?.download_overview?.appid ? topics.download_overview : null);
const kernel = $derived(agent.state?.power?.battery);
function controllerName(model: string | null) {
if (model?.endsWith("_Left")) return "Left controller";
if (model?.endsWith("_Right")) return "Right controller";
return model ?? "Controller";
}
</script>
{#if !agent.state}
<p class="empty">
{agent.status === "unconfigured" ? "Set up the connection in Settings." : "Connecting to your Frame…"}
</p>
{:else}
<Section title="Steam Frame">
<div class="hero">
<Icon name="headset" size={56} />
<div class="hero-text">
{#if battery}
<div class="big">
{percent(battery.level)}
<Battery level={battery.level} {charging} size={26} />
</div>
<div class="muted">
{charging
? `Charging · full in ${duration(battery.seconds_remaining)}`
: `About ${duration(battery.seconds_remaining)} left`}
</div>
{:else}
<div class="big">–</div>
{/if}
<div class="muted">
{hmdActivity(topics?.vr_state?.hmd_activity)}
{#if kernel?.power_w != null}· {Math.abs(kernel.power_w).toFixed(1)} W{/if}
</div>
</div>
</div>
</Section>
{#if controllers.length}
<Section title="Controllers">
<div class="controllers">
{#each controllers as c (c.path)}
<div class="controller">
<Icon name="controller" size={28} />
<div>
<div>{controllerName(c.model)}</div>
<div class="muted small">
{percent(c.battery)}
{c.charging ? "· charging" : c.connected ? "" : "· asleep"}
</div>
</div>
<span class="spacer"></span>
<Battery level={c.battery} charging={!!c.charging} size={18} />
</div>
{/each}
</div>
</Section>
{/if}
<Section title="Now playing">
{#if running.length}
{#each running as app (app.appid)}
<div class="card">
<GameArt appid={app.appid} />
<div class="card-body">{app.name ?? app.appid}</div>
</div>
{/each}
{:else}
<p class="empty">Nothing running</p>
{/if}
</Section>
<Section title="Downloading">
{#snippet aside()}<a href="/downloads" class="more">All downloads ›</a>{/snippet}
{#if download?.appid}
<div class="card">
<GameArt appid={download.appid} />
<div class="card-body">
<div class="line">
<span>{download.name ?? download.appid}{download.is_workshop ? " · Workshop" : ""}</span>
<span class="muted">{download.percent ?? 0}%</span>
</div>
<Progress value={download.percent} paused={download.paused} />
<div class="line muted small">
<span>{download.paused ? "Paused" : `${bytes(download.bytes_per_sec)}/s`}</span>
<span>{download.eta_sec ? `${duration(download.eta_sec)} left` : ""}</span>
</div>
</div>
</div>
{:else}
<p class="empty">No active downloads</p>
{/if}
</Section>
{/if}
<style>
.hero {
display: flex;
align-items: center;
gap: 20px;
padding: 20px var(--gutter);
background: linear-gradient(135deg, #2a475e, var(--surface));
}
.hero-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.big {
display: flex;
align-items: center;
gap: 10px;
font-size: 32px;
font-weight: 600;
}
.controllers {
display: flex;
flex-direction: column;
gap: 1px;
}
.controller {
display: flex;
align-items: center;
gap: 14px;
padding: 12px var(--gutter);
background: var(--surface);
}
.spacer {
flex: 1;
}
.small {
font-size: 13px;
}
.card {
background: var(--surface);
}
.card + .card {
margin-top: 8px;
}
.card-body {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px var(--gutter);
}
.line {
display: flex;
justify-content: space-between;
gap: 12px;
}
.more {
font-size: 13px;
color: var(--muted);
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<script lang="ts">
import { agent } from "$lib/agent.svelte";
import GameArt from "$lib/components/GameArt.svelte";
import Progress from "$lib/components/Progress.svelte";
import Section from "$lib/components/Section.svelte";
import { bytes, duration } from "$lib/format";
const topics = $derived(agent.state?.steam.topics);
const overview = $derived(topics?.download_overview);
const items = $derived(topics?.downloads ?? []);
const queued = $derived(items.filter(i => !i.completed).sort((a, b) => a.queue_index - b.queue_index));
const finished = $derived(items.filter(i => i.completed).sort((a, b) => b.completed_time - a.completed_time));
function finishedAgo(unixSeconds: number) {
return unixSeconds ? `${duration(Date.now() / 1000 - unixSeconds)} ago` : "";
}
</script>
<Section title="Queue">
{#if queued.length}
{#each queued as item (item.appid)}
{@const live = overview?.appid === item.appid ? overview : null}
<div class="item">
<GameArt appid={item.appid} kind="capsule" />
<div class="body">
<div class="line">
<span class="name">{item.name ?? item.appid}</span>
<span class="muted">{live?.percent ?? item.percent ?? 0}%</span>
</div>
<Progress value={live?.percent ?? item.percent} paused={item.paused} />
<div class="line muted small">
<span>
{#if item.error}Error: {item.error}
{:else if item.paused}Paused
{:else if live}{bytes(live.bytes_per_sec)}/s
{:else}Queued{/if}
</span>
<span>{live?.eta_sec ? `${duration(live.eta_sec)} left` : ""}</span>
</div>
</div>
</div>
{/each}
{:else}
<p class="empty">Nothing queued</p>
{/if}
</Section>
{#if finished.length}
<Section title="Recently finished">
{#each finished as item (item.appid)}
<div class="item">
<GameArt appid={item.appid} kind="capsule" />
<div class="body">
<span class="name">{item.name ?? item.appid}</span>
<span class="muted small">Updated {finishedAgo(item.completed_time)}</span>
</div>
</div>
{/each}
</Section>
{/if}
<style>
.item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px var(--gutter);
background: var(--surface);
border-bottom: 1px solid var(--divider);
}
.body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.line {
display: flex;
justify-content: space-between;
gap: 8px;
}
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.small {
font-size: 13px;
}
</style>
+81
View File
@@ -0,0 +1,81 @@
<script lang="ts">
import { agent } from "$lib/agent.svelte";
import Row from "$lib/components/Row.svelte";
import Section from "$lib/components/Section.svelte";
let host = $state(agent.settings.host);
let token = $state(agent.settings.token);
const statusText = $derived(
{
unconfigured: "Enter host and token",
connecting: "Connecting…",
connected: `Connected to ${agent.state?.agent.hostname ?? agent.authority}`,
offline: `Can't reach ${agent.authority}`,
}[agent.status],
);
function save(event: SubmitEvent) {
event.preventDefault();
agent.save({ host, token });
}
</script>
<Section title="Connection">
<form onsubmit={save}>
<label>
<span>Frame address</span>
<input bind:value={host} placeholder="frame.local or 192.168.x.x" autocapitalize="off" autocorrect="off" spellcheck="false" />
</label>
<label>
<span>Token</span>
<input bind:value={token} placeholder="from the agent log / dashboard link" autocapitalize="off" autocorrect="off" spellcheck="false" />
</label>
<button class="button" type="submit">Save &amp; connect</button>
<p class="status {agent.status}">{statusText}</p>
</form>
</Section>
<Section title="About">
<Row label="Agent version" value={agent.state?.agent.version} />
</Section>
<style>
form {
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px var(--gutter);
background: var(--surface);
}
label {
display: flex;
flex-direction: column;
gap: 6px;
}
label span {
font-size: 13px;
color: var(--muted);
}
input {
min-height: 44px;
padding: 0 12px;
background: var(--surface-2);
border: 1px solid transparent;
border-radius: 2px;
}
input:focus {
outline: none;
border-color: var(--accent);
}
.status {
margin: 0;
color: var(--muted);
}
.status.connected {
color: var(--green);
}
.status.offline {
color: var(--red);
}
</style>
+64
View File
@@ -0,0 +1,64 @@
<script lang="ts">
import { agent } from "$lib/agent.svelte";
import Row from "$lib/components/Row.svelte";
import Section from "$lib/components/Section.svelte";
import { bytes, hmdActivity, since } from "$lib/format";
const s = $derived(agent.state);
const topics = $derived(s?.steam.topics);
const props = $derived(s?.steamos.properties ?? {});
const kernel = $derived(s?.power?.battery);
const info = $derived(topics?.system_info);
const net = $derived(topics?.network);
const perf = $derived(topics?.perf);
const yesNo = (v: boolean | null | undefined) => (v == null ? null : v ? "Yes" : "No");
</script>
{#if s}
<Section title="Headset">
<Row label="Status" value={hmdActivity(topics?.vr_state?.hmd_activity)} />
<Row label="VR app" value={topics?.vr_state?.scene_app_name ?? "None"} />
<Row label="Power draw" value={kernel?.power_w != null ? `${Math.abs(kernel.power_w).toFixed(1)} W` : null} />
<Row label="Battery temperature" value={kernel?.temp_c != null ? `${kernel.temp_c.toFixed(1)} °C` : null} />
<Row label="Battery health" value={kernel ? `${kernel.health ?? "–"} · ${kernel.cycle_count ?? "–"} cycles` : null} />
</Section>
<Section title="Performance">
<Row label="Profile" value={props.PerformanceProfile} />
<Row label="CPU governor" value={props.CpuScalingGovernor} />
<Row label="CPU scheduler" value={props.CpuScheduler} />
<Row label="GPU performance" value={props.GpuPerformanceLevel} />
<Row label="TDP limit" value={perf?.tdp_limit_w != null ? `${perf.tdp_limit_w} W` : "Off"} />
<Row label="Frame limit" value={perf?.fps_limit ?? "Off"} />
</Section>
<Section title="Internet">
<Row label="Internet" value={yesNo(net?.internet)} />
<Row label="Steam" value={yesNo(net?.steam)} />
{#each net?.interfaces ?? [] as i (i.name)}
<Row label={i.name} value={`↓ ${bytes(i.rx_bytes_per_sec)}/s · ↑ ${bytes(i.tx_bytes_per_sec)}/s`} />
{/each}
</Section>
<Section title="Software">
<Row label="SteamOS" value={info ? `${info.os_version} (${info.os_variant})` : null} />
<Row label="OS build" value={info?.os_build} />
<Row label="Steam client" value={info?.steam_version} />
<Row label="Kernel" value={info?.kernel} />
<Row label="Graphics" value={info ? `${info.gpu} · ${info.gpu_driver}` : null} />
<Row label="Memory" value={info ? `${(info.ram_mb / 1024).toFixed(0)} GB` : null} />
</Section>
<Section title="Account">
<Row label="Signed in as" value={topics?.user?.persona_name} />
<Row label="Account" value={topics?.user?.account_name} />
</Section>
<Section title="FrameMate agent">
<Row label="Version" value={s.agent.version} />
<Row label="Running for" value={since(s.agent.started_at_ms)} />
<Row label="Steam connection" value={s.steam.connected ? "Connected" : (s.steam.error ?? "Not connected")} />
</Section>
{:else}
<p class="empty">Not connected</p>
{/if}
+188
View File
@@ -0,0 +1,188 @@
<script lang="ts">
// Live headset view. Same protocol as the agent's /stream page: a JSON header with the
// codec string, an fMP4 init segment, then one moof+mdat per frame, fed into MSE.
// The agent only captures while a viewer is connected, so leaving this tab stops it.
// The app is portrait-only; fullscreen switches to landscape (natively on Android).
import { onDestroy, onMount } from "svelte";
import { pushState } from "$app/navigation";
import { page } from "$app/state";
import { agent } from "$lib/agent.svelte";
import Icon from "$lib/components/Icon.svelte";
import Row from "$lib/components/Row.svelte";
import Section from "$lib/components/Section.svelte";
let video: HTMLVideoElement;
let status = $state("Connecting…");
let socket: WebSocket | null = null;
let retry: ReturnType<typeof setTimeout> | undefined;
let closed = false;
const stats = $derived(agent.state?.stream);
const fullscreen = $derived(!!page.state.fullscreen);
// Fullscreen is a shallow history entry: Android's back button (and Esc on desktop)
// leave it like any other navigation.
function enterFullscreen() {
pushState("", { fullscreen: true });
}
function applyFullscreen(enabled: boolean) {
if (window.FrameMateAndroid) {
window.FrameMateAndroid.setFullscreen(enabled);
} else if (enabled && !document.fullscreenElement) {
document.documentElement.requestFullscreen?.().catch(() => {});
} else if (!enabled && document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
}
$effect(() => applyFullscreen(fullscreen));
// Leaving browser fullscreen via Esc: drop our history entry too.
function onFullscreenChange() {
if (!document.fullscreenElement && page.state.fullscreen) history.back();
}
function connect() {
const ws = new WebSocket(agent.socketUrl("/api/stream/ws"));
socket = ws;
ws.binaryType = "arraybuffer";
let buffer: SourceBuffer | null = null;
const queue: ArrayBuffer[] = [];
const pump = () => {
if (buffer && !buffer.updating && queue.length) buffer.appendBuffer(queue.shift()!);
};
ws.onmessage = event => {
if (typeof event.data !== "string") {
queue.push(event.data);
pump();
return;
}
const { codec } = JSON.parse(event.data);
const type = `video/mp4; codecs="${codec}"`;
if (!MediaSource.isTypeSupported(type)) {
status = `This device can't play ${codec}`;
closed = true;
ws.close();
return;
}
const source = new MediaSource();
video.src = URL.createObjectURL(source);
source.addEventListener(
"sourceopen",
() => {
buffer = source.addSourceBuffer(type);
buffer.mode = "sequence";
buffer.addEventListener("updateend", () => {
keepLive(buffer!);
pump();
});
pump();
},
{ once: true },
);
status = "";
};
ws.onclose = () => {
if (closed) return;
status = "Reconnecting…";
retry = setTimeout(connect, 2000);
};
}
// Catch up by playing slightly faster; seeking on every frame restarts decoding at the
// last keyframe and drops the picture to a few fps.
function keepLive(buffer: SourceBuffer) {
if (buffer.updating || !buffer.buffered.length) return;
const start = buffer.buffered.start(0);
const end = buffer.buffered.end(buffer.buffered.length - 1);
const behind = end - video.currentTime;
if (video.currentTime < start || behind > 2) video.currentTime = Math.max(start, end - 0.2);
video.playbackRate = behind > 0.3 ? 1.1 : 1.0;
if (video.paused) video.play().catch(() => {});
if (video.currentTime - start > 10) buffer.remove(start, video.currentTime - 5);
}
onMount(connect);
onDestroy(() => {
if (fullscreen) applyFullscreen(false);
closed = true;
clearTimeout(retry);
socket?.close();
});
</script>
<svelte:document onfullscreenchange={onFullscreenChange} />
<div class="player" class:fullscreen>
<video bind:this={video} autoplay muted playsinline></video>
{#if status}<div class="overlay">{status}</div>{/if}
{#if fullscreen}
<button class="control exit" onclick={() => history.back()} aria-label="Exit fullscreen">
<Icon name="back" size={22} />
</button>
{:else}
<button class="control enter" onclick={enterFullscreen} aria-label="Fullscreen">
<svg viewBox="0 0 36 36" width="22" height="22" fill="currentColor" aria-hidden="true">
<path d="M4 4h11v4H8v7H4zM21 4h11v11h-4V8h-7zM4 21h4v7h7v4H4zM28 21h4v11H21v-4h7z" />
</svg>
</button>
{/if}
</div>
<Section title="Stream">
<Row label="Resolution" value={stats ? `${stats.width}×${stats.height}` : null} />
<Row label="Frame rate" value={stats ? `${stats.fps.toFixed(0)} fps (headset ${stats.source_fps.toFixed(0)})` : null} />
<Row label="Bitrate" value={stats ? `${(stats.kbit_per_sec / 1000).toFixed(1)} Mbit/s` : null} />
<Row label="Viewers" value={stats?.viewers} />
</Section>
<p class="hint muted">The headset only renders this view while someone watches. A sleeping headset shows black frames.</p>
<style>
.player {
position: relative;
background: #000;
aspect-ratio: 16 / 9;
}
video {
width: 100%;
height: 100%;
display: block;
}
.player.fullscreen {
position: fixed;
inset: 0;
z-index: 10;
aspect-ratio: auto;
}
.control {
position: absolute;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 2px;
background: rgb(0 0 0 / 0.5);
color: #fff;
}
.enter {
right: 8px;
bottom: 8px;
}
.exit {
left: 12px;
top: 12px;
}
.overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--muted);
}
.hint {
padding: 12px var(--gutter);
font-size: 13px;
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg3"
sodipodi:docname="frame-mate-friendly-frame.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs3" />
<sodipodi:namedview
id="namedview3"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="true"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#202020"
inkscape:zoom="1.1582031"
inkscape:cx="374.71839"
inkscape:cy="313.41653"
inkscape:window-width="1720"
inkscape:window-height="1372"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg3"
inkscape:export-bgcolor="#ffffff00"
showborder="true" />
<title
id="title1">Frame-Mate Friendly Frame</title>
<desc
id="desc1">A friendly companion version of the Steam Frame-inspired logo, with a smiling face inside the frame.</desc>
<!-- Frame -->
<rect
style="display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:31.1766;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="426.48282"
height="426.48282"
x="46.212227"
y="43.622009"
inkscape:label="rect1"
rx="48" />
<path
d="M 79.770658,81.497471 C 88.802511,66.421772 440,72 440,72 v 72 c 0,0 -287.53399,-8.46601 -296,0 -8.46601,8.46601 0,296 0,296 H 72 c 0,0 -1.261196,-343.426831 7.770658,-358.502529 z"
fill="#000000"
id="path1"
sodipodi:nodetypes="zcczccz"
style="fill:#ffffff;fill-opacity:1" />
<!-- Blue companion shape -->
<path
d="m 208,440 c 0,-128.13006 103.86994,-232 232,-232 0,0 3.28372,219.57202 -3.45363,230.27319 C 429.80902,448.97435 208,440 208,440 Z"
fill="#2196f3"
id="path2"
sodipodi:nodetypes="cczc" />
<!-- Friendly face -->
<circle
cx="180.0067"
cy="185.06584"
r="23.654644"
fill="#000000"
id="circle2"
style="stroke-width:1.81959;fill:#ffffff;fill-opacity:1" />
<circle
cx="322.91068"
cy="185.06584"
r="23.654644"
fill="#000000"
id="circle3"
style="stroke-width:1.81959;fill:#ffffff;fill-opacity:1" />
<path
d="m 217.29542,224.1911 c 14.60088,23.48211 53.53662,23.48211 68.13747,0"
fill="none"
stroke="#000000"
stroke-width="10.7716"
stroke-linecap="round"
id="path3"
style="stroke-width:21.3045;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1" />
<metadata
id="metadata1">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Frame-Mate Friendly Frame</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+18
View File
@@ -0,0 +1,18 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we use adapter-static with a fallback to index.html to put the site in SPA mode
// See: https://svelte.dev/docs/kit/single-page-apps
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: "index.html",
}),
},
};
export default config;
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
// @ts-expect-error type error without @types/node package
import process from "node:process";
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(() => ({
plugins: [sveltekit()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || "127.0.0.1",
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+95
View File
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg3"
sodipodi:docname="frame-mate-friendly-frame.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs3" />
<sodipodi:namedview
id="namedview3"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="true"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#202020"
inkscape:zoom="1.1582031"
inkscape:cx="374.71839"
inkscape:cy="313.41653"
inkscape:window-width="1720"
inkscape:window-height="1372"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg3"
inkscape:export-bgcolor="#ffffff00"
showborder="true" />
<title
id="title1">Frame-Mate Friendly Frame</title>
<desc
id="desc1">A friendly companion version of the Steam Frame-inspired logo, with a smiling face inside the frame.</desc>
<!-- Frame -->
<rect
style="display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:31.1766;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="426.48282"
height="426.48282"
x="46.212227"
y="43.622009"
inkscape:label="rect1"
rx="48" />
<path
d="M 79.770658,81.497471 C 88.802511,66.421772 440,72 440,72 v 72 c 0,0 -287.53399,-8.46601 -296,0 -8.46601,8.46601 0,296 0,296 H 72 c 0,0 -1.261196,-343.426831 7.770658,-358.502529 z"
fill="#000000"
id="path1"
sodipodi:nodetypes="zcczccz"
style="fill:#ffffff;fill-opacity:1" />
<!-- Blue companion shape -->
<path
d="m 208,440 c 0,-128.13006 103.86994,-232 232,-232 0,0 3.28372,219.57202 -3.45363,230.27319 C 429.80902,448.97435 208,440 208,440 Z"
fill="#2196f3"
id="path2"
sodipodi:nodetypes="cczc" />
<!-- Friendly face -->
<circle
cx="180.0067"
cy="185.06584"
r="23.654644"
fill="#000000"
id="circle2"
style="stroke-width:1.81959;fill:#ffffff;fill-opacity:1" />
<circle
cx="322.91068"
cy="185.06584"
r="23.654644"
fill="#000000"
id="circle3"
style="stroke-width:1.81959;fill:#ffffff;fill-opacity:1" />
<path
d="m 217.29542,224.1911 c 14.60088,23.48211 53.53662,23.48211 68.13747,0"
fill="none"
stroke="#000000"
stroke-width="10.7716"
stroke-linecap="round"
id="path3"
style="stroke-width:21.3045;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1" />
<metadata
id="metadata1">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Frame-Mate Friendly Frame</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more