feat(mobile): run on Android, and release a signed APK (#1044)
* feat(mobile): run on Android The app builds and runs on Android, and fits there. - Back button and gesture close what is open over the screen, then go back a screen, and from the first one send the app to the background. Left to the WebView they closed the app, since it has no history. - The system bars: the page asks their size of the app (`insets`, over JNI), since WebViews before 140 report the safe areas as 0 even edge to edge. The CSS reads them as `--inset-*`, which iOS still fills from `env()`. - The keyboard: edge to edge, the WebView is not resized for it and its visual viewport stays whole, so the keyboard covered the message box. MainActivity hands its height to the page, which lays out as on iOS. - The title folds into the bar from the scroll position on every screen. The observer it used reported a title in plain view as out of sight on Android, so the bar started folded. - src-tauri/gen/android is kept in the repo for that MainActivity. * ci(mobile): release a signed Android APK A `mobile-v<x.y.z>` tag builds the app for arm64 at that version, signs it with the release key and attaches it to a draft release. The mobile app is versioned apart from the desktop's `v*` tags. - The release is never marked latest: the desktop updater reads /releases/latest and would take it for a desktop release. - The APK's certificate is checked against the release key's, since one signed with another key could not be installed over earlier ones. - The NDK is pinned, and the version must be x.y.z: Tauri derives the versionCode from it, and Android installs over a build only when that is higher. - Gradle signs a release build when keystore.properties names a key; CI writes it from secrets. - The release library is stripped: 30 MB to 20, the APK 32 to 23. * ci: tell people how to install the phone app The nightly release notes and each mobile-v release now say how to get the app on a phone: the TestFlight link for iPhone, and the newest mobile-v release's APK for Android. Both read .github/mobile-install.md.
@@ -0,0 +1,6 @@
|
||||
## Phone app
|
||||
|
||||
Watch and drive this computer's panes and agents from your phone. On the computer, open **Settings → Mobile**, turn on **Allow phone access** and click **Show code**, then pair from the app by scanning the code or pasting it.
|
||||
|
||||
- **iPhone**: join the beta at https://testflight.apple.com/join/gFcPbPVR. The link asks you to install TestFlight from the App Store first, if you don't have it.
|
||||
- **Android** (arm64): on the phone, download `tty7-<version>-android-arm64.apk` from the newest [`mobile-v*` release](https://github.com/l0ng-ai/tty7/releases?q=mobile-v&expanded=true) and open it. Android asks once to allow installing apps from your browser or file manager. A later APK installs over it and keeps your paired machines.
|
||||
@@ -0,0 +1,140 @@
|
||||
name: Mobile
|
||||
|
||||
# The phone app's Android build: a signed APK people install by hand. A
|
||||
# `mobile-v<x.y.z>` tag builds it at that version and attaches it to a draft
|
||||
# release of the same name; a manual run builds it at the version in
|
||||
# tauri.conf.json and keeps it as a workflow artifact only.
|
||||
#
|
||||
# The mobile app is versioned apart from the desktop's `v*` tags. Android
|
||||
# installs one build over another only when its versionCode is higher, and
|
||||
# Tauri derives it from the version (major * 1000000 + minor * 1000 + patch),
|
||||
# so each tag must be higher than the last.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["mobile-v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
# The release key's certificate. A build signed with any other key could not
|
||||
# be installed over the ones people already have, so it is refused here.
|
||||
CERT_SHA256: c54b8b466e7ac45bac2c9d79e44ec3e6ab8fef1d00818adafa11bb747163d75f
|
||||
NDK_VERSION: 28.2.13676358
|
||||
|
||||
jobs:
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$GITHUB_REF" == refs/tags/mobile-v* ]]; then
|
||||
VERSION="${GITHUB_REF_NAME#mobile-v}"
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::tag must be mobile-v<major>.<minor>.<patch>, got $GITHUB_REF_NAME"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
VERSION=$(jq -r .version mobile/src-tauri/tauri.conf.json)
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: mobile/package-lock.json
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: mobile/src-tauri
|
||||
|
||||
# The runner has an SDK and some NDK; the NDK is pinned so a runner image
|
||||
# update cannot change the toolchain under a release.
|
||||
- name: Android NDK
|
||||
run: |
|
||||
set -euo pipefail
|
||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "ndk;$NDK_VERSION" > /dev/null
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Signing key
|
||||
env:
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
|
||||
echo "::error::the ANDROID_KEYSTORE_* secrets are not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
|
||||
# Read by gen/android/app/build.gradle.kts; git ignores it.
|
||||
{
|
||||
echo "storeFile=$RUNNER_TEMP/release.jks"
|
||||
echo "storePassword=$ANDROID_KEYSTORE_PASSWORD"
|
||||
echo "keyAlias=$ANDROID_KEY_ALIAS"
|
||||
echo "keyPassword=$ANDROID_KEYSTORE_PASSWORD"
|
||||
} > mobile/src-tauri/gen/android/keystore.properties
|
||||
|
||||
# arm64 only: every phone of the last several years, with one copy of
|
||||
# the native library instead of four.
|
||||
- name: Build
|
||||
working-directory: mobile
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm ci
|
||||
npx tauri android build --apk --target aarch64 --config "{\"version\":\"$VERSION\"}"
|
||||
|
||||
- name: Check and name the APK
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK=mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
|
||||
APKSIGNER=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)/apksigner
|
||||
CERT=$("$APKSIGNER" verify --print-certs "$APK" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')
|
||||
if [ "$CERT" != "$CERT_SHA256" ]; then
|
||||
echo "::error::APK is signed with $CERT, not the release key"
|
||||
exit 1
|
||||
fi
|
||||
mkdir dist
|
||||
cp "$APK" "dist/tty7-$VERSION-android-arm64.apk"
|
||||
(cd dist && sha256sum *.apk > SHA256SUMS)
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tty7-android-${{ env.VERSION }}
|
||||
path: dist/*
|
||||
|
||||
# A draft, published by hand as the desktop's are. Never marked latest:
|
||||
# the desktop updater reads /releases/latest and would take this for a
|
||||
# desktop release.
|
||||
- name: Draft release
|
||||
if: startsWith(github.ref, 'refs/tags/mobile-v')
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXISTING=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \
|
||||
--json tagName -q '.[].tagName')
|
||||
if grep -Fxq "$GITHUB_REF_NAME" <<<"$EXISTING"; then
|
||||
echo "release $GITHUB_REF_NAME already exists; reusing it"
|
||||
else
|
||||
gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \
|
||||
--draft --latest=false --title "tty7 mobile $VERSION" \
|
||||
--notes-file .github/mobile-install.md
|
||||
fi
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber --repo "$GITHUB_REPOSITORY"
|
||||
@@ -546,7 +546,9 @@ jobs:
|
||||
git push -f origin "$GITHUB_SHA:refs/tags/nightly"
|
||||
|
||||
TITLE="Nightly $VERSION"
|
||||
NOTES="Automated nightly build of \`main\` @ ${GITHUB_SHA::7} ($(date -u +%F)). Rolling prerelease — assets are replaced every night; for the latest stable release see https://github.com/${GITHUB_REPOSITORY}/releases/latest."
|
||||
NOTES="Automated nightly build of \`main\` @ ${GITHUB_SHA::7} ($(date -u +%F)). Rolling prerelease — assets are replaced every night; for the latest stable release see https://github.com/${GITHUB_REPOSITORY}/releases/latest.
|
||||
|
||||
$(cat .github/mobile-install.md)"
|
||||
|
||||
if gh release view nightly >/dev/null 2>&1; then
|
||||
gh release edit nightly --prerelease --title "$TITLE" --notes "$NOTES"
|
||||
|
||||
@@ -83,13 +83,35 @@ last. `--build-number 7` picks it.
|
||||
|
||||
### Android
|
||||
|
||||
Needs Android Studio's SDK and NDK, with `ANDROID_HOME` and `NDK_HOME` set.
|
||||
Needs the Android SDK and NDK, with `ANDROID_HOME` and `NDK_HOME` set, and a JDK 17 to 21
|
||||
as `JAVA_HOME`.
|
||||
|
||||
```sh
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
|
||||
cd mobile
|
||||
npm run tauri android init
|
||||
npm run tauri android dev
|
||||
npm run tauri android dev # emulator or a connected phone
|
||||
npm run tauri android build -- --debug --apk --target aarch64 # an installable .apk
|
||||
```
|
||||
|
||||
`src-tauri/gen/android` is kept in the repo, unlike `gen/apple`: its `MainActivity` hands the
|
||||
keyboard's height to the page, which the WebView does not report edge to edge. Don't re-run
|
||||
`android init` over it.
|
||||
|
||||
To release an APK, push a `mobile-v<x.y.z>` tag, higher than the last. `.github/workflows/mobile.yml`
|
||||
builds it at that version, signs it with the release key and attaches it to a draft
|
||||
release, which is never marked latest, so the desktop updater doesn't see it. People
|
||||
download the APK on the phone and open it. A later one installs over it only if it's
|
||||
signed with the same key and its version is higher.
|
||||
|
||||
The release key is in the `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD` and
|
||||
`ANDROID_KEY_ALIAS` secrets, with a copy kept outside GitHub. A local release build signs
|
||||
with it when `src-tauri/gen/android/keystore.properties` (ignored by git) names it:
|
||||
|
||||
```properties
|
||||
storeFile=/path/to/tty7-release.jks
|
||||
storePassword=…
|
||||
keyAlias=tty7
|
||||
keyPassword=…
|
||||
```
|
||||
|
||||
### Without a phone
|
||||
|
||||
@@ -5809,6 +5809,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"iroh",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"rustls",
|
||||
"serde",
|
||||
|
||||
@@ -38,6 +38,16 @@ tauri-plugin-barcode-scanner = "2"
|
||||
# Face ID / passcode before the app shows anything, when turned on.
|
||||
tauri-plugin-biometric = "2"
|
||||
|
||||
# Reading the system bars' size and leaving for the background (`insets`,
|
||||
# `to_background`), which the Tauri API does not reach.
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = "0.21"
|
||||
|
||||
# Setting one WKWebView property the Tauri API does not reach (`edge_to_edge`).
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc2 = "0.6"
|
||||
|
||||
# The Android library ships as built: stripping takes it from 30 MB to 20, and
|
||||
# the APK from 32 to 23. iOS strips at export either way.
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,93 @@
|
||||
import java.util.Properties
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
// Release signing, from keystore.properties beside this project (ignored by
|
||||
// git): `storeFile`, `storePassword`, `keyAlias`, `keyPassword`. CI writes it
|
||||
// from secrets (.github/workflows/mobile.yml). Without it a release build
|
||||
// comes out unsigned, and a debug build uses the machine's debug key.
|
||||
val keystoreProperties = Properties().apply {
|
||||
val propFile = rootProject.file("keystore.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "dev.tty7.mobile"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "dev.tty7.mobile"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
if (keystoreProperties.containsKey("storeFile")) {
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
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") {
|
||||
signingConfigs.findByName("release")?.let { signingConfig = it }
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
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 = "tauri.build.gradle.kts")
|
||||
@@ -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,37 @@
|
||||
<?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.tty7_mobile"
|
||||
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: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,30 @@
|
||||
package dev.tty7.mobile
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
// Edge to edge, the WebView is never resized for the keyboard, and it does
|
||||
// not shrink the page's visual viewport either: the page could not tell the
|
||||
// keyboard is up, and it would cover the message box. Its height is handed
|
||||
// to the page instead (main.ts `android-keyboard`), which then lays out as
|
||||
// it does on iOS. The WebView still sees the insets itself.
|
||||
override fun onWebViewCreate(webView: WebView) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(webView) { view, insets ->
|
||||
val keyboard = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom / view.resources.displayMetrics.density
|
||||
webView.evaluateJavascript(
|
||||
"window.dispatchEvent(new CustomEvent('android-keyboard', { detail: $keyboard }))",
|
||||
null,
|
||||
)
|
||||
ViewCompat.onApplyWindowInsets(view, insets)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.tty7_mobile" parent="Theme.MaterialComponents.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 @@
|
||||
<resources>
|
||||
<string name="app_name">"tty7"</string>
|
||||
<string name="main_activity_title">"tty7"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.tty7_mobile" parent="Theme.MaterialComponents.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:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
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:8.11.0")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """npm""";
|
||||
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("run", "--", "tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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
|
||||
# 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
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
@@ -0,0 +1,6 @@
|
||||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# 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"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# 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"
|
||||
which java >/dev/null 2>&1 || 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
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@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
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
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%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,3 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
@@ -540,8 +540,22 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
hosts, pair, forget, watch, unwatch, refresh, tab_new, upload, diff, pane_open,
|
||||
pane_input, pane_lease, pane_close, appearance
|
||||
hosts,
|
||||
pair,
|
||||
forget,
|
||||
watch,
|
||||
unwatch,
|
||||
refresh,
|
||||
tab_new,
|
||||
upload,
|
||||
diff,
|
||||
pane_open,
|
||||
pane_input,
|
||||
pane_lease,
|
||||
pane_close,
|
||||
appearance,
|
||||
insets,
|
||||
to_background
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tty7");
|
||||
@@ -597,3 +611,144 @@ fn edge_to_edge(window: &tauri::WebviewWindow) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The system bars and display cutout the page is drawn under, in CSS pixels.
|
||||
/// Android runs the WebView edge to edge, yet WebViews before 140 report
|
||||
/// `env(safe-area-inset-*)` as 0, so the page asks here. Zero elsewhere: iOS's
|
||||
/// WebView reports its own.
|
||||
#[derive(Serialize, Default)]
|
||||
struct Insets {
|
||||
top: f64,
|
||||
right: f64,
|
||||
bottom: f64,
|
||||
left: f64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn insets(window: tauri::WebviewWindow) -> Insets {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let _ = window.with_webview(move |webview| {
|
||||
webview.jni_handle().exec(move |env, activity, _| {
|
||||
let insets = android::insets(env, activity).unwrap_or_else(|_| {
|
||||
let _ = env.exception_clear();
|
||||
Insets::default()
|
||||
});
|
||||
let _ = tx.send(insets);
|
||||
})
|
||||
});
|
||||
rx.await.unwrap_or_default()
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = window;
|
||||
Insets::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Back from the first screen: the app goes to the background, as Android's
|
||||
/// own apps do, rather than being closed.
|
||||
#[tauri::command]
|
||||
fn to_background(window: tauri::WebviewWindow) {
|
||||
#[cfg(target_os = "android")]
|
||||
let _ = window.with_webview(|webview| {
|
||||
webview.jni_handle().exec(|env, activity, _| {
|
||||
if env
|
||||
.call_method(activity, "moveTaskToBack", "(Z)Z", &[true.into()])
|
||||
.is_err()
|
||||
{
|
||||
let _ = env.exception_clear();
|
||||
}
|
||||
})
|
||||
});
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let _ = window;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android {
|
||||
use jni::JNIEnv;
|
||||
use jni::errors::Result;
|
||||
use jni::objects::JObject;
|
||||
|
||||
use super::Insets;
|
||||
|
||||
pub fn insets(env: &mut JNIEnv, activity: &JObject) -> Result<Insets> {
|
||||
let window = env
|
||||
.call_method(activity, "getWindow", "()Landroid/view/Window;", &[])?
|
||||
.l()?;
|
||||
let decor = env
|
||||
.call_method(&window, "getDecorView", "()Landroid/view/View;", &[])?
|
||||
.l()?;
|
||||
let insets = env
|
||||
.call_method(
|
||||
&decor,
|
||||
"getRootWindowInsets",
|
||||
"()Landroid/view/WindowInsets;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
// Not attached yet: nothing to avoid.
|
||||
if insets.is_null() {
|
||||
return Ok(Insets::default());
|
||||
}
|
||||
let sdk = env
|
||||
.get_static_field("android/os/Build$VERSION", "SDK_INT", "I")?
|
||||
.i()?;
|
||||
let [top, right, bottom, left] = if sdk >= 30 {
|
||||
let types = "android/view/WindowInsets$Type";
|
||||
let bars = env
|
||||
.call_static_method(types, "systemBars", "()I", &[])?
|
||||
.i()?;
|
||||
let cutout = env
|
||||
.call_static_method(types, "displayCutout", "()I", &[])?
|
||||
.i()?;
|
||||
let edges = env
|
||||
.call_method(
|
||||
&insets,
|
||||
"getInsets",
|
||||
"(I)Landroid/graphics/Insets;",
|
||||
&[(bars | cutout).into()],
|
||||
)?
|
||||
.l()?;
|
||||
let mut side = |name| env.get_field(&edges, name, "I").and_then(|v| v.i());
|
||||
[side("top")?, side("right")?, side("bottom")?, side("left")?]
|
||||
} else {
|
||||
let mut side = |name| {
|
||||
env.call_method(&insets, name, "()I", &[])
|
||||
.and_then(|v| v.i())
|
||||
};
|
||||
[
|
||||
side("getSystemWindowInsetTop")?,
|
||||
side("getSystemWindowInsetRight")?,
|
||||
side("getSystemWindowInsetBottom")?,
|
||||
side("getSystemWindowInsetLeft")?,
|
||||
]
|
||||
};
|
||||
let resources = env
|
||||
.call_method(
|
||||
activity,
|
||||
"getResources",
|
||||
"()Landroid/content/res/Resources;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
let metrics = env
|
||||
.call_method(
|
||||
&resources,
|
||||
"getDisplayMetrics",
|
||||
"()Landroid/util/DisplayMetrics;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
let density = f64::from(env.get_field(&metrics, "density", "F")?.f()?).max(1.0);
|
||||
let css = |px: i32| f64::from(px) / density;
|
||||
Ok(Insets {
|
||||
top: css(top),
|
||||
right: css(right),
|
||||
bottom: css(bottom),
|
||||
left: css(left),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,19 @@ export const forget = (hostId: string) => invoke<void>("forget", { hostId });
|
||||
/** Sets the style of what the page does not draw: status bar, keyboard. */
|
||||
export const appearance = (style: "system" | "light" | "dark") => invoke<void>("appearance", { style });
|
||||
|
||||
export interface Insets {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
/** What the system bars cover, in CSS pixels. Android only: zero elsewhere. */
|
||||
export const insets = () => invoke<Insets>("insets");
|
||||
|
||||
/** Sends the app to the background, as Back from the first screen does. */
|
||||
export const toBackground = () => invoke<void>("to_background");
|
||||
|
||||
export function watch(hostId: string, onMsg: (msg: TreeMsg) => void) {
|
||||
const onEvent = new Channel<TreeMsg>();
|
||||
onEvent.onmessage = onMsg;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { authenticate, checkStatus } from "@tauri-apps/plugin-biometric";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import * as scanner from "@tauri-apps/plugin-barcode-scanner";
|
||||
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { getVersion, onBackButtonPress } from "@tauri-apps/api/app";
|
||||
|
||||
import * as api from "./api";
|
||||
import { MAX_UPLOAD } from "./api";
|
||||
@@ -29,17 +29,21 @@ import logoUrl from "./assets/logo.svg?url";
|
||||
|
||||
const app = document.getElementById("app")!;
|
||||
|
||||
const android = /Android/.test(navigator.userAgent);
|
||||
|
||||
// The keyboard. The WebView runs edge to edge and is never resized for it
|
||||
// (lib.rs `edge_to_edge`): the keyboard simply covers the bottom of the page.
|
||||
// What is left is the visual viewport, so the app is sized to that, and the
|
||||
// dock and the message box sit on top of the keyboard. Screens that lay out
|
||||
// by size hear it as a window resize.
|
||||
// by size hear it as a window resize. Android's WebView leaves the visual
|
||||
// viewport whole, so the keyboard's height comes from the app (MainActivity).
|
||||
{
|
||||
const view = window.visualViewport;
|
||||
let last = 0;
|
||||
let androidKeyboard = 0;
|
||||
const fitView = () => {
|
||||
if (!view) return;
|
||||
const height = Math.round(view.height);
|
||||
const height = Math.round(android ? window.innerHeight - androidKeyboard : view.height);
|
||||
// iOS scrolls the page to show a focused field; the app does its own.
|
||||
if (window.scrollY) window.scrollTo(0, 0);
|
||||
if (height === last) return;
|
||||
@@ -53,6 +57,26 @@ const app = document.getElementById("app")!;
|
||||
};
|
||||
view?.addEventListener("resize", fitView);
|
||||
view?.addEventListener("scroll", fitView);
|
||||
window.addEventListener("android-keyboard", (e) => {
|
||||
androidKeyboard = (e as CustomEvent<number>).detail;
|
||||
fitView();
|
||||
});
|
||||
}
|
||||
|
||||
// Android's system bars. The WebView runs under them edge to edge, but older
|
||||
// WebViews report the safe areas as 0, so their size is asked of the system
|
||||
// (style.css `--inset-*`). Asked again on a resize: turning the phone moves them.
|
||||
if (android) {
|
||||
const fitInsets = () =>
|
||||
api
|
||||
.insets()
|
||||
.then((insets) => {
|
||||
for (const [side, px] of Object.entries(insets))
|
||||
document.documentElement.style.setProperty(`--inset-${side}`, `${px}px`);
|
||||
})
|
||||
.catch(() => {});
|
||||
void fitInsets();
|
||||
window.addEventListener("resize", () => void fitInsets());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -317,6 +341,24 @@ document.addEventListener(
|
||||
);
|
||||
document.addEventListener("touchcancel", () => (edgeSwipe = null), { capture: true, passive: true });
|
||||
|
||||
// Android's Back button and gesture. Left to the WebView, they would leave the
|
||||
// app, since it has no history. Back closes what is open over the screen
|
||||
// first, then goes back a screen, and from the first one puts the app away.
|
||||
if (android)
|
||||
void onBackButtonPress(() => {
|
||||
const scanning = document.querySelector<HTMLElement>(".scanner-cancel");
|
||||
const sheet = [...document.querySelectorAll<HTMLElement>("body > .scrim:not(.leaving)")].pop();
|
||||
const menu = document.querySelector(".menu:not([hidden])");
|
||||
if (document.querySelector(".lock-cover")) void api.toBackground();
|
||||
else if (scanning) scanning.click();
|
||||
// Tapping the scrim itself, outside the sheet, is what closes it.
|
||||
else if (sheet) sheet.click();
|
||||
// A menu closes on a press anywhere outside it.
|
||||
else if (menu) document.body.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
|
||||
else if (onBack) onBack();
|
||||
else void api.toBackground();
|
||||
}).catch(() => {});
|
||||
|
||||
interface ScreenParts {
|
||||
title: string;
|
||||
back?: { label: string; onclick: () => void };
|
||||
@@ -362,20 +404,15 @@ function screen(parts: ScreenParts) {
|
||||
h("div", { class: "title-block" }, heading, parts.subtitle),
|
||||
...parts.body,
|
||||
);
|
||||
if (root) {
|
||||
// The bar floats over a top-level screen, empty until the title has
|
||||
// scrolled up under it.
|
||||
scroll.addEventListener(
|
||||
"scroll",
|
||||
() => bar.classList.toggle("folded", scroll.scrollTop > large.offsetTop + large.offsetHeight - bar.offsetHeight),
|
||||
{ passive: true },
|
||||
);
|
||||
} else {
|
||||
new IntersectionObserver(
|
||||
([entry]) => bar.classList.toggle("folded", !entry.isIntersecting),
|
||||
{ root: scroll, threshold: 0, rootMargin: "-8px 0px 0px 0px" },
|
||||
).observe(large);
|
||||
}
|
||||
// The title folds into the bar once it has scrolled up under it. On a
|
||||
// top-level screen the bar floats over the list, empty until then. Worked
|
||||
// out from the scroll, not observed: Android's WebView reported a title in
|
||||
// plain view as out of sight when it was observed.
|
||||
scroll.addEventListener(
|
||||
"scroll",
|
||||
() => bar.classList.toggle("folded", scroll.scrollTop > large.offsetTop + large.offsetHeight - bar.offsetHeight),
|
||||
{ passive: true },
|
||||
);
|
||||
const view = h("div", { class: "screen" }, bar, scroll, parts.footer, parts.dock);
|
||||
if (parts.dock) view.classList.add("docked");
|
||||
if (root) view.classList.add("root");
|
||||
|
||||
@@ -55,7 +55,14 @@
|
||||
|
||||
/* The home indicator's gap; none while the keyboard is up, which covers
|
||||
it (main.ts sets `keyboard` on <html>). */
|
||||
--safe-bottom: env(safe-area-inset-bottom);
|
||||
--safe-bottom: var(--inset-bottom);
|
||||
|
||||
/* The system bars. main.ts sets these on Android, whose older WebViews
|
||||
report the safe areas as 0 even edge to edge. */
|
||||
--inset-top: env(safe-area-inset-top);
|
||||
--inset-right: env(safe-area-inset-right);
|
||||
--inset-bottom: env(safe-area-inset-bottom);
|
||||
--inset-left: env(safe-area-inset-left);
|
||||
|
||||
color-scheme: light;
|
||||
font: 17px/1.35 var(--ui);
|
||||
@@ -196,9 +203,9 @@ code {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: calc(44px + env(safe-area-inset-top));
|
||||
padding: env(safe-area-inset-top) max(16px, env(safe-area-inset-right)) 0
|
||||
max(16px, env(safe-area-inset-left));
|
||||
height: calc(44px + var(--inset-top));
|
||||
padding: var(--inset-top) max(16px, var(--inset-right)) 0
|
||||
max(16px, var(--inset-left));
|
||||
background: var(--canvas);
|
||||
box-shadow: 0 1px 0 transparent;
|
||||
transition:
|
||||
@@ -326,8 +333,8 @@ code {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0 max(0px, env(safe-area-inset-right)) calc(32px + var(--safe-bottom))
|
||||
max(0px, env(safe-area-inset-left));
|
||||
padding: 0 max(0px, var(--inset-right)) calc(32px + var(--safe-bottom))
|
||||
max(0px, var(--inset-left));
|
||||
}
|
||||
|
||||
.title-block {
|
||||
@@ -351,7 +358,7 @@ code {
|
||||
}
|
||||
|
||||
.screen.root .scroll {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-top: var(--inset-top);
|
||||
}
|
||||
|
||||
.screen.root .title-block {
|
||||
@@ -945,7 +952,7 @@ html.scanning #app {
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: env(safe-area-inset-top) 24px calc(28px + var(--safe-bottom));
|
||||
padding: var(--inset-top) 24px calc(28px + var(--safe-bottom));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -1054,8 +1061,8 @@ html.scanning #app {
|
||||
|
||||
.dock {
|
||||
flex: none;
|
||||
padding: 12px max(16px, env(safe-area-inset-right)) calc(12px + var(--safe-bottom))
|
||||
max(16px, env(safe-area-inset-left));
|
||||
padding: 12px max(16px, var(--inset-right)) calc(12px + var(--safe-bottom))
|
||||
max(16px, var(--inset-left));
|
||||
background: var(--canvas);
|
||||
}
|
||||
|
||||
@@ -1198,9 +1205,9 @@ html.scanning #app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: calc(56px + env(safe-area-inset-top));
|
||||
padding: env(safe-area-inset-top) max(16px, env(safe-area-inset-right)) 0
|
||||
max(16px, env(safe-area-inset-left));
|
||||
height: calc(56px + var(--inset-top));
|
||||
padding: var(--inset-top) max(16px, var(--inset-right)) 0
|
||||
max(16px, var(--inset-left));
|
||||
background: var(--term);
|
||||
box-shadow: 0 0.5px 0 var(--hair);
|
||||
position: relative;
|
||||
@@ -1405,8 +1412,8 @@ html.scanning #app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px max(12px, env(safe-area-inset-right)) calc(10px + var(--safe-bottom))
|
||||
max(12px, env(safe-area-inset-left));
|
||||
padding: 10px max(12px, var(--inset-right)) calc(10px + var(--safe-bottom))
|
||||
max(12px, var(--inset-left));
|
||||
background: var(--canvas);
|
||||
box-shadow: 0 -0.5px 0 var(--hair);
|
||||
}
|
||||
@@ -1550,8 +1557,8 @@ html.scanning #app {
|
||||
.float-bar {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
left: max(16px, env(safe-area-inset-left));
|
||||
right: max(16px, env(safe-area-inset-right));
|
||||
left: max(16px, var(--inset-left));
|
||||
right: max(16px, var(--inset-right));
|
||||
bottom: calc(12px + var(--safe-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1667,7 +1674,7 @@ html.scanning #app {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-height: calc(100% - env(safe-area-inset-top) - 24px);
|
||||
max-height: calc(100% - var(--inset-top) - 24px);
|
||||
padding: 10px 20px calc(20px + var(--safe-bottom));
|
||||
border-radius: 44px;
|
||||
background: var(--canvas);
|
||||
|
||||