Newer
Older
navi-1 / webclient / src / components / settings / AppPanel.vue
<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>