<template>
<div class="app-panel">
<h2 class="app-title">App</h2>
<p class="app-description">
Navi is a web app, but it can live on your device like a native one — either
installed into this browser, or as the Android app, which wraps this same
interface. They are not exclusive: the Android app is worth having on a phone,
the installed web app everywhere else.
</p>
<!-- Deliberately a plain bordered block, not a kit GnCard: that one is a
compact 340px dashboard tile, and the sibling settings panels are built
the same way. -->
<section class="app-card">
<h3 class="app-card-title">
<i class="ph ph-app-window"></i>
<span>Install in this browser</span>
</h3>
<p class="app-text">
Installing puts Navi in its own window with no address bar, adds an icon to
your dock or home screen, and caches the interface so the app opens instantly
and still shows your sessions offline. It also enables notifications for
finished replies — including when the tab is closed.
</p>
<GnAlert v-if="state === 'android-app'" variant="info" class="app-alert">
<i class="ph ph-info"></i>
<span>
You are reading this inside the Android app, which is already installed.
To update it, download the APK below and install it over the old one.
</span>
</GnAlert>
<GnAlert v-else-if="state === 'installed'" variant="success" class="app-alert">
<i class="ph ph-check-circle"></i>
<span>Installed — Navi is running in its own window, not in a browser tab.</span>
</GnAlert>
<div v-else-if="state === 'prompt'" class="app-action">
<GnButton
variant="primary"
icon="ph ph-download-simple"
:loading="installing"
@click="onInstall"
>
Install
</GnButton>
<span class="app-hint">Your browser will ask you to confirm.</span>
</div>
<ol v-else class="app-steps">
<li v-if="isIos">
<b>Safari:</b> tap <i class="ph ph-export"></i> Share, then
<b>Add to Home Screen</b>. Notifications there need iOS 16.4 or newer and
work only in the installed app, not in a Safari tab.
</li>
<li v-else>
<b>Chrome or Edge:</b> click the install icon at the right end of the
address bar, or open the <i class="ph ph-dots-three-vertical"></i> menu and
choose <b>Install Navi</b>.
</li>
<li v-if="!isIos">
<b>Android:</b> open the <i class="ph ph-dots-three-vertical"></i> menu and
choose <b>Install app</b> (sometimes “Add to Home screen”).
</li>
<li>
Browsers only offer this over HTTPS or on localhost — over a plain
<code>http://</code> address the option never appears, and neither do
offline mode and notifications.
</li>
</ol>
</section>
<section class="app-card">
<h3 class="app-card-title">
<i class="ph ph-android-logo"></i>
<span>Android app</span>
</h3>
<p class="app-text">
The same Navi in a native shell. What a browser tab cannot give you:
notifications while the phone is locked — a background service keeps the
connection open — plus a native camera and photo picker, and links that open
in your normal browser. Sessions, profiles and settings are the server's, so
the app and the web client stay in step.
</p>
<p v-if="!apk" class="app-hint">
The APK is not published on this server — build it with
<code>android-client/tools/publish-apk.sh</code>.
</p>
<template v-else>
<dl class="app-meta">
<div><dt>Version</dt><dd>{{ apk.versionName }}</dd></div>
<div><dt>Size</dt><dd>{{ sizeText }}</dd></div>
<div><dt>Built</dt><dd>{{ dateText }}</dd></div>
<div class="app-meta-sha">
<dt>SHA-256</dt>
<dd><code>{{ apk.sha256 }}</code></dd>
</div>
</dl>
<GnButton variant="primary" icon="ph ph-android-logo" @click="downloadApk">
Download APK
</GnButton>
<ol class="app-steps app-steps-install">
<li>
Open the downloaded file on the phone. Android will ask whether to allow
installing apps from this source — allow it.
</li>
<li>
On first launch the app asks for your Navi address. Enter
<code>{{ defaultOrigin }}</code
>, then log in through the browser when it opens; the app returns on its
own.
</li>
<li>
A newer APK installs over the old one and keeps your settings. You would
only have to uninstall first if the signing key ever changed.
</li>
</ol>
</template>
</section>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { GnAlert, GnButton } from 'gnexus-ui-kit/vue'
import { usePwa } from '@/composables/usePwa.js'
const { state, install, isIos } = usePwa()
const installing = ref(false)
const apk = ref(null)
// Where this page is served from is the address the Android app should be
// pointed at — showing it beats making the reader guess.
const defaultOrigin = window.location.origin
const sizeText = computed(() =>
apk.value ? `${(apk.value.size / 1048576).toFixed(1)} MB` : ''
)
const dateText = computed(() =>
apk.value
? new Date(apk.value.builtAt).toLocaleDateString(undefined, {
day: 'numeric',
month: 'short',
year: 'numeric',
})
: ''
)
onMounted(async () => {
try {
const res = await fetch('/download/navi-android.json')
if (res.ok) apk.value = await res.json()
} catch {
// Offline, or the download directory was never published: the card says so.
}
})
async function onInstall() {
installing.value = true
try {
await install()
} finally {
installing.value = false
}
}
function downloadApk() {
// ?download=1 is not decoration: the Android app hands a same-host URL
// carrying it to the system browser, and a WebView cannot install an APK.
window.location.assign('/download/navi-android.apk?download=1')
}
</script>
<style scoped lang="scss">
.app-panel {
max-width: 720px;
margin-bottom: 24px;
}
.app-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 12px;
color: var(--gn-color-text-medium);
}
.app-description {
margin: 0 0 16px;
font-size: 0.875rem;
color: var(--gn-color-text-medium);
}
.app-card {
padding: 14px 16px;
margin-bottom: 16px;
border: 1px solid var(--gn-border-color-muted);
border-radius: 8px;
}
.app-card-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 10px;
font-size: 0.9rem;
font-weight: 600;
color: var(--gn-color-text-medium);
}
.app-text {
margin: 0 0 12px;
font-size: 0.875rem;
color: var(--gn-color-text-medium);
}
.app-alert {
display: flex;
align-items: flex-start;
gap: 8px;
i {
font-size: 16px;
flex-shrink: 0;
}
}
.app-action {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.app-hint {
margin: 0;
font-size: 0.8rem;
color: var(--gn-color-text-dark);
}
.app-steps {
margin: 0;
padding-left: 18px;
font-size: 0.8rem;
color: var(--gn-color-text-medium);
li + li {
margin-top: 6px;
}
code {
font-size: 0.75rem;
padding: 1px 4px;
border-radius: 4px;
background: var(--gn-color-surface-sunken, rgba(127, 127, 127, 0.12));
}
}
.app-steps-install {
margin-top: 14px;
}
.app-meta {
margin: 0 0 14px;
font-size: 0.8rem;
color: var(--gn-color-text-dark);
> div {
display: flex;
gap: 8px;
}
dt {
min-width: 64px;
color: var(--gn-color-text-medium);
}
dd {
margin: 0;
}
}
.app-meta-sha code {
font-size: 0.7rem;
word-break: break-all;
}
</style>