Something went wrong. Try again.
a love letter to tangled (android, iOS, and a search API)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279<template> <ion-page> <page-header :back="repositoryBack" :title="isCompare ? 'Compare' : 'Changes'" /> <ion-content :fullscreen="true"> <main id="page-content" class="diff-page page-frame"> <repository-navigation :current-ref="head || singleRef" :repo="repo" /> <request-state :error="repositoryRequest.error.value" :loading="repositoryRequest.phase.value === 'loading'" noun="repository" @retry="repositoryRequest.retry" />
<header v-if="repository" class="diff-page__header"> <div> <p class="section-label">{{ repository.value.name || 'Repository' }}</p> <h1>{{ isCompare ? 'Compare revisions' : 'Changes in this revision' }}</h1> <p v-if="isCompare && base && head"> Changes needed to move from <code>{{ base }}</code> to <code>{{ head }}</code>. </p> <p v-else-if="singleRef">Changes introduced by <code>{{ singleRef }}</code>.</p> </div> <a v-if="downloadUrl" class="secondary-action" :href="downloadUrl">Download raw patch</a> </header>
<form v-if="isCompare" class="compare-form" @submit.prevent="compare"> <label> <span>Base</span> <input v-model="baseDraft" list="repository-refs" placeholder="main or a commit hash" required /> </label> <button class="swap-button" type="button" aria-label="Swap base and head" @click="swap">⇄</button> <label> <span>Head</span> <input v-model="headDraft" list="repository-refs" placeholder="feature branch or a commit hash" required /> </label> <button type="submit">Compare</button> <datalist id="repository-refs"> <option v-for="option in refOptions" :key="option" :value="option" /> </datalist> </form>
<request-state :empty="patchRequest.phase.value === 'empty'" empty-message="These revisions do not have any changes to show." :error="patchRequest.error.value" :loading="patchRequest.phase.value === 'loading'" noun="changes" @retry="patchRequest.retry" />
<section v-if="patch?.kind === 'too-large'" class="large-patch" aria-labelledby="large-patch-heading"> <h2 id="large-patch-heading">This patch is too large to display safely</h2> <p> Download the raw patch to inspect it in your editor<span v-if="patch.bytes"> ({{ formatBytes(patch.bytes) }})</span >. </p> <a v-if="downloadUrl" class="primary-action" :href="downloadUrl">Download raw patch</a> </section> <patch-viewer v-else-if="patch?.kind === 'patch' && patch.text" :patch="patch.text" /> </main> </ion-content> </ion-page></template>
<script setup lang="ts">import PageHeader from '@/components/PageHeader.vue'import RequestState from '@/components/RequestState.vue'import PatchViewer from '@/features/repositories/PatchViewer.vue'import RepositoryNavigation from '@/features/repositories/RepositoryNavigation.vue'import { useRepositoryRoute } from '@/features/repositories/useRepositoryRoute'import type { RepositoryPatch } from '@/lib/api'import { useRouteRequest } from '@/lib/requests'import { links } from '@/lib/router/links'import { IonContent, IonPage } from '@ionic/vue'import { computed, ref, watch } from 'vue'import { useRouter } from 'vue-router'
const { getClient, repo, repoUri, repository, repositoryRequest, route } = useRepositoryRoute()const router = useRouter()const isCompare = computed(() => route.name === 'repository-compare')const singleRef = computed(() => String(route.params.ref ?? ''))const base = computed(() => String(route.query.base ?? ''))const head = computed(() => String(route.query.head ?? ''))const baseDraft = ref(base.value)const headDraft = ref(head.value)watch([base, head], ([nextBase, nextHead]) => { baseDraft.value = nextBase headDraft.value = nextHead})const repositoryBack = computed(() => router.resolve(links.repository(repo.value)).href)
const branchesRequest = useRouteRequest(repoUri, (uri, signal, attempt) => uri ? getClient().listRepositoryBranches(uri, { signal, cache: attempt.cache, limit: 99 }) : Promise.resolve({ items: [] }),)const tagsRequest = useRouteRequest(repoUri, (uri, signal, attempt) => uri ? getClient().listRepositoryTags(uri, { signal, cache: attempt.cache, limit: 99 }) : Promise.resolve({ items: [] }),)const refOptions = computed(() => [ ...(branchesRequest.data.value?.items.map((item) => item.name) ?? []), ...(tagsRequest.data.value?.items.map((item) => item.name) ?? []),])
const patchSource = computed(() => ({ uri: repoUri.value, compare: isCompare.value, base: base.value, head: head.value, ref: singleRef.value,}))const patchRequest = useRouteRequest( patchSource, (source, signal, attempt): Promise<RepositoryPatch | undefined> => { if (!source.uri) return Promise.resolve(undefined) if (source.compare) { if (!source.base || !source.head) return Promise.resolve(undefined) return getClient().getRepositoryCompare(source.uri, source.base, source.head, { signal, cache: attempt.cache, }) } if (!source.ref) return Promise.resolve(undefined) return getClient().getRepositoryDiff(source.uri, source.ref, { signal, cache: attempt.cache }) }, { isEmpty: (value) => value === undefined || (value.kind === 'patch' && value.text.trim().length === 0) },)const patch = computed(() => patchRequest.data.value)const downloadUrl = computed(() => { if (!repoUri.value) return '' return isCompare.value ? base.value && head.value ? getClient().repositoryCompareUrl(repoUri.value, base.value, head.value) : '' : singleRef.value ? getClient().repositoryDiffUrl(repoUri.value, singleRef.value) : ''})
function compare() { const nextBase = baseDraft.value.trim() const nextHead = headDraft.value.trim() if (nextBase && nextHead) void router.replace(links.compare(repo.value, nextBase, nextHead))}
function swap() { const previousBase = baseDraft.value baseDraft.value = headDraft.value headDraft.value = previousBase compare()}
function formatBytes(bytes: number) { return new Intl.NumberFormat(undefined, { style: 'unit', unit: 'kilobyte', maximumFractionDigits: 0 }).format( bytes / 1024, )}</script>
<style scoped>.diff-page { display: grid; gap: var(--space-6);}
.diff-page__header { display: flex; align-items: end; justify-content: space-between; gap: var(--space-5);}
.diff-page__header h1,.diff-page__header p,.large-patch h2,.large-patch p { margin: 0;}
.diff-page__header div > p:last-child { margin-block-start: var(--space-2); color: var(--app-text-muted);}
.compare-form { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto; align-items: end; gap: var(--space-3); border: 1px solid var(--app-border); border-radius: var(--radius-md); padding: var(--space-4); background: var(--app-surface);}
.compare-form label { display: grid; gap: var(--space-2); color: var(--app-text-muted); font-size: var(--text-sm); font-weight: 700;}
.compare-form input,.compare-form button,.primary-action,.secondary-action { min-block-size: 44px; border: 1px solid var(--app-border); border-radius: var(--radius-sm); padding-inline: var(--space-4); color: var(--app-text); background: var(--app-background);}
.compare-form button,.primary-action { border-color: var(--app-accent); color: var(--app-accent-contrast); background: var(--app-accent); font-weight: 800; cursor: pointer;}
.compare-form .swap-button { inline-size: 44px; padding: 0; border-color: var(--app-border); color: var(--app-text); background: transparent; font-size: var(--text-lg);}
.primary-action,.secondary-action { display: inline-flex; align-items: center; justify-content: center; text-decoration: none;}
.secondary-action { flex: 0 0 auto; background: var(--app-surface);}
.large-patch { display: grid; justify-items: start; gap: var(--space-3); border: 1px solid var(--app-warning); border-radius: var(--radius-md); padding: var(--space-6); background: var(--app-surface);}
.large-patch p { color: var(--app-text-muted);}
@media (max-width: 760px) { .diff-page__header { align-items: start; flex-direction: column; } .compare-form { grid-template-columns: 1fr; } .compare-form label { grid-column: auto; } .compare-form .swap-button { justify-self: start; } .compare-form button[type='submit'] { grid-column: auto; }}</style>