Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8460ad9dce |
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "com.archipelago.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 18
|
||||
versionName = "0.4.14"
|
||||
versionCode = 17
|
||||
versionName = "0.4.13"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
||||
@@ -239,12 +239,7 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) {
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
// CameraX's analysis default is 640x480 — too few pixels per module
|
||||
// to decode a modal-sized QR at arm's length. 1280x720 more than
|
||||
// doubles the pixel density at negligible analysis cost.
|
||||
@Suppress("DEPRECATION")
|
||||
val analysis = ImageAnalysis.Builder()
|
||||
.setTargetResolution(android.util.Size(1280, 720))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also {
|
||||
@@ -273,14 +268,7 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) {
|
||||
/** ZXing-based QR decoder over the camera's Y (luminance) plane. */
|
||||
private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAnalysis.Analyzer {
|
||||
private val reader = MultiFormatReader().apply {
|
||||
setHints(
|
||||
mapOf(
|
||||
DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE),
|
||||
// Screen-displayed QRs come with moiré, glare, and soft focus at
|
||||
// close range — the exhaustive search is worth the milliseconds.
|
||||
DecodeHintType.TRY_HARDER to true,
|
||||
)
|
||||
)
|
||||
setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)))
|
||||
}
|
||||
|
||||
override fun analyze(image: ImageProxy) {
|
||||
@@ -296,13 +284,7 @@ private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAna
|
||||
0, 0, image.width, image.height,
|
||||
false,
|
||||
)
|
||||
val result = try {
|
||||
reader.decodeWithState(BinaryBitmap(HybridBinarizer(source)))
|
||||
} catch (_: NotFoundException) {
|
||||
// Dark-themed pages can render light-on-dark QRs — retry inverted.
|
||||
reader.reset()
|
||||
reader.decodeWithState(BinaryBitmap(HybridBinarizer(source.invert())))
|
||||
}
|
||||
val result = reader.decodeWithState(BinaryBitmap(HybridBinarizer(source)))
|
||||
onDecoded(result.text)
|
||||
} catch (_: NotFoundException) {
|
||||
// No QR in this frame — keep scanning.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
@@ -59,7 +59,6 @@
|
||||
class="flex-1 inline-flex items-center justify-center rounded-lg bg-orange-500/20 border border-orange-500/30 px-3 py-2.5 text-sm font-medium text-orange-400 hover:bg-orange-500/30 transition-colors text-center"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
download
|
||||
>
|
||||
Download app
|
||||
</a>
|
||||
@@ -93,9 +92,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<!-- Bigger than the download QR: this one is scanned by the
|
||||
companion app camera, and physical size drives decode. -->
|
||||
<div class="w-[192px] rounded-2xl border border-white/10 bg-white/[0.03] p-1 overflow-hidden">
|
||||
<div class="w-[128px] rounded-2xl border border-white/10 bg-white/[0.03] p-1 overflow-hidden">
|
||||
<img
|
||||
v-if="pairQrDataUrl"
|
||||
:src="pairQrDataUrl"
|
||||
@@ -135,7 +132,6 @@
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import * as QRCode from 'qrcode'
|
||||
import { IS_DEMO, DEMO_PASSWORD } from '@/composables/useDemoIntro'
|
||||
import { companionIntroRequested } from '@/composables/useCompanionIntro'
|
||||
import { useLoginTransitionStore } from '@/stores/loginTransition'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
@@ -206,26 +202,11 @@ function maybeShow() {
|
||||
}, 250)
|
||||
}
|
||||
|
||||
// Manual open (App Store banner etc.) — ignores the once-per-browser gate.
|
||||
watch(companionIntroRequested, (requested) => {
|
||||
if (!requested) return
|
||||
companionIntroRequested.value = false
|
||||
if (calmTicker) {
|
||||
clearInterval(calmTicker)
|
||||
calmTicker = null
|
||||
}
|
||||
step.value = 'download'
|
||||
visible.value = true
|
||||
})
|
||||
|
||||
watch(visible, async (isVisible) => {
|
||||
if (!isVisible) return
|
||||
// Generate large and let CSS scale down — at 112px source a ~45-module QR
|
||||
// is 2.5px/module, which camera decoders (the companion app included)
|
||||
// routinely fail on. 512px keeps every module crisp.
|
||||
qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, {
|
||||
width: 512,
|
||||
margin: 2,
|
||||
width: 112,
|
||||
margin: 1,
|
||||
errorCorrectionLevel: 'M',
|
||||
color: {
|
||||
dark: '#111111',
|
||||
@@ -266,11 +247,9 @@ async function showPairScreen() {
|
||||
step.value = 'pair'
|
||||
if (!pairQrDataUrl.value) {
|
||||
pairingUrl.value = await buildPairingUrl()
|
||||
// Large source + a real quiet zone; this QR is scanned by the companion
|
||||
// app's camera, so give it every advantage (see download QR note above).
|
||||
pairQrDataUrl.value = await QRCode.toDataURL(pairingUrl.value, {
|
||||
width: 512,
|
||||
margin: 3,
|
||||
width: 112,
|
||||
margin: 1,
|
||||
errorCorrectionLevel: 'M',
|
||||
color: {
|
||||
dark: '#111111',
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Cross-view trigger for the Remote Companion intro/pairing modal
|
||||
* (CompanionIntroOverlay, mounted once in Dashboard.vue). Views like the
|
||||
* App Store banner call openCompanionIntro() to pop it on demand — this
|
||||
* bypasses the once-per-browser auto-show gate.
|
||||
*/
|
||||
export const companionIntroRequested = ref(false)
|
||||
|
||||
export function openCompanionIntro(): void {
|
||||
companionIntroRequested.value = true
|
||||
}
|
||||
@@ -150,9 +150,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile companion app banner — opens the download/pairing modal -->
|
||||
<CompanionBanner />
|
||||
|
||||
<!-- Category Section Divider -->
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<span class="discover-terminal-tag">all</span>
|
||||
@@ -242,7 +239,6 @@ import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout
|
||||
import { APP_STORE_SECTIONS } from './appStoreCategories'
|
||||
import DiscoverHero from './discover/DiscoverHero.vue'
|
||||
import FeaturedApps from './discover/FeaturedApps.vue'
|
||||
import CompanionBanner from './discover/CompanionBanner.vue'
|
||||
import AppGrid from './discover/AppGrid.vue'
|
||||
import InstallVersionModal from '@/components/InstallVersionModal.vue'
|
||||
import type { MarketplaceApp, FeaturedApp } from './discover/types'
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<!-- Companion app banner — same format as the featured app banner, with a
|
||||
phone mockup rising out of the right edge. Clicking anywhere (or the
|
||||
Install button) opens the Remote Companion download/pairing modal. -->
|
||||
<div
|
||||
class="featured-banner companion-banner glass-card mb-8 relative overflow-hidden cursor-pointer"
|
||||
@click="openCompanionIntro()"
|
||||
>
|
||||
<img
|
||||
src="/assets/img/bg-intro-4.webp"
|
||||
alt=""
|
||||
class="featured-banner-img"
|
||||
@error="(e: Event) => ((e.target as HTMLImageElement).style.display = 'none')"
|
||||
/>
|
||||
<div class="featured-banner-overlay companion-banner-overlay">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="discover-terminal-tag">companion</span>
|
||||
<span class="text-white/50 text-sm font-mono">REMOTE CONTROL // IN YOUR POCKET</span>
|
||||
</div>
|
||||
<h2 class="text-3xl md:text-4xl font-extrabold text-white mb-2 tracking-tight">Your Node. In Your Pocket.</h2>
|
||||
<p class="text-white/80 text-base md:text-lg max-w-2xl leading-relaxed mb-4">
|
||||
The Archipelago Companion puts your node on your phone — remote control and typing,
|
||||
your cloud files on the go, and one scan to connect. Sovereignty doesn't stay home.
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click.stop="openCompanionIntro()"
|
||||
class="glass-button rounded-lg px-6 py-2.5 text-sm font-medium"
|
||||
>Install</button>
|
||||
<span class="text-white/40 text-sm">Archipelago Companion · Android</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="companion-phone-wrap" aria-hidden="true">
|
||||
<div class="companion-phone">
|
||||
<span class="companion-phone-lens"></span>
|
||||
<img src="/assets/img/companion-phone-screen.webp" alt="" class="companion-phone-screen" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { openCompanionIntro } from '@/composables/useCompanionIntro'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.companion-banner {
|
||||
border-color: rgba(251, 146, 60, 0.25);
|
||||
}
|
||||
|
||||
/* Keep the copy clear of the phone mockup on wide screens */
|
||||
@media (min-width: 768px) {
|
||||
.companion-banner-overlay {
|
||||
padding-right: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.companion-phone-wrap {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.companion-phone-wrap {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 3.5rem;
|
||||
z-index: 2;
|
||||
transform: rotate(6deg);
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
.companion-banner:hover .companion-phone-wrap {
|
||||
transform: rotate(4deg) translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
.companion-phone {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
padding: 9px;
|
||||
border-radius: 30px;
|
||||
background: #0b0b12;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow:
|
||||
0 24px 60px rgba(0, 0, 0, 0.55),
|
||||
0 0 46px rgba(251, 146, 60, 0.28),
|
||||
inset 0 0 2px rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Punch-hole camera */
|
||||
.companion-phone-lens {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 9999px;
|
||||
background: #000;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.companion-phone-screen {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
/* Screen glare */
|
||||
.companion-phone::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 9px;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(115deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.04) 28%, transparent 45%);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -37,9 +37,7 @@ export default defineConfig({
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
// /packages/ must bypass the SPA fallback — otherwise clicking the
|
||||
// companion APK download link gets index.html instead of the file.
|
||||
navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//, /^\/packages\//],
|
||||
navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//],
|
||||
cleanupOutdatedCaches: true,
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,mp4,webp}'],
|
||||
globIgnores: [
|
||||
|
||||
Reference in New Issue
Block a user