Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions build/android/app/src/main/java/com/wails/app/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -436,23 +436,37 @@ public void copyToFolder(final String json) {
java.io.File source = new java.io.File(o.getString("sourcePath"));
if (!source.exists()) {
bridge.emitEvent("android:copyDone",
"{\"ok\":false,\"error\":\"staging file missing\"}");
"{\"ok\":false,\"error\":" + JSONObject.quote("staging file missing") + "}");
return;
}

if (!DocumentsContract.isTreeUri(treeUri)) {
throw new IllegalArgumentException("Invalid folder URI");
}

// createDocument expects a document URI. The folder picker
// returns a tree URI, so first resolve that tree's root
// document URI before asking the provider to create a file.
String treeDocumentId = DocumentsContract.getTreeDocumentId(treeUri);
Uri parentDocumentUri = DocumentsContract.buildDocumentUriUsingTree(
treeUri, treeDocumentId);

// Create the destination document in the chosen folder.
String mime = android.webkit.MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(fileName.contains(".")
? fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
: "");
Uri docUri = DocumentsContract.createDocument(getContentResolver(),
treeUri, mime != null ? mime : "application/octet-stream", fileName);
parentDocumentUri, mime != null ? mime : "application/octet-stream", fileName);
if (docUri == null) {
throw new IllegalStateException("could not create destination file");
}

try (InputStream in = new FileInputStream(source);
OutputStream out = getContentResolver().openOutputStream(docUri)) {
if (out == null) {
bridge.emitEvent("android:copyDone",
"{\"ok\":false,\"error\":\"cannot open destination\"}");
"{\"ok\":false,\"error\":" + JSONObject.quote("cannot open destination") + "}");
return;
}
byte[] buf = new byte[64 * 1024];
Expand All @@ -464,12 +478,13 @@ public void copyToFolder(final String json) {
// Staging copy is no longer needed.
boolean deleted = source.delete();
bridge.emitEvent("android:copyDone",
"{\"ok\":true,\"fileName\":\"" + fileName.replace("\"", "\\\"")
+ "\",\"deleted\":" + deleted + "}");
"{\"ok\":true,\"fileName\":" + JSONObject.quote(fileName)
+ ",\"deleted\":" + deleted + "}");
} catch (Exception e) {
Log.e(TAG, "copyToFolder failed", e);
bridge.emitEvent("android:copyDone",
"{\"ok\":false,\"error\":\"" + e.getMessage() + "\"}");
"{\"ok\":false,\"error\":"
+ JSONObject.quote(e.getMessage() != null ? e.getMessage() : "copy failed") + "}");
}
}).start();
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/common/Toast.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ const { toasts } = useUI()
</script>

<template>
<div class="pointer-events-none fixed inset-x-0 top-4 z-[200] flex flex-col items-center gap-2 px-4">
<div class="pointer-events-none fixed inset-x-0 top-[max(1rem,env(safe-area-inset-top))] z-[200] flex flex-col items-center gap-2 px-4">
<TransitionGroup name="toast">
<div
v-for="t in toasts"
:key="t.id"
class="glass pointer-events-auto flex items-center gap-2 rounded-lg px-4 py-2 text-sm shadow-lg"
class="glass pointer-events-auto flex max-w-[calc(100vw-2rem)] items-center gap-2 rounded-lg px-4 py-2 text-sm shadow-lg"
:class="t.type === 'error' ? 'text-danger' : t.type === 'success' ? 'text-ok' : 'text-content'"
>
<Icon v-if="t.type === 'error'" name="warning" :size="16" />
Expand Down
51 changes: 34 additions & 17 deletions frontend/src/components/layout/AppLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,58 +16,75 @@ const nav = [
</script>

<template>
<div class="flex h-full flex-col bg-ink-deep md:flex-row">
<!-- Desktop sidebar -->
<aside class="hidden w-72 shrink-0 flex-col border-r border-white/5 bg-surface-0 md:flex">
<div class="drag-region border-b border-white/5 px-5 py-5"></div>
<div class="flex h-[100dvh] min-h-0 flex-col bg-ink-deep md:flex-row">
<!-- Tablet rail / desktop sidebar -->
<aside class="hidden w-[76px] shrink-0 flex-col border-r border-white/5 bg-surface-0 md:flex lg:w-64">
<div class="drag-region flex min-h-20 items-center border-b border-white/5 px-3 lg:px-5">
<div class="brand-lockup">
<span class="brand-mark">L</span>
<div class="hidden min-w-0 lg:block">
<div class="text-sm font-bold tracking-[0.18em] text-content">LIGHT</div>
<div class="text-[9px] uppercase tracking-[0.16em] text-content-faint">Local transfer</div>
</div>
</div>
</div>

<nav class="flex flex-col gap-1 px-3">
<nav class="flex flex-col gap-1 px-2 pt-4 lg:px-3">
<RouterLink
v-for="n in nav"
:key="n.to"
:to="n.to"
class="nav-item"
:title="n.label"
class="nav-item justify-center lg:justify-start"
active-class="nav-item--active"
>
<Icon :name="n.icon" />
<span>{{ n.label }}</span>
<Icon :name="n.icon" :size="20" />
<span class="hidden lg:inline">{{ n.label }}</span>
</RouterLink>
</nav>

<div class="mt-auto border-t border-white/5 p-3">
<button class="btn-ghost w-full justify-start" @click="showPair = true">
<Icon name="qrcode" />
<span>Pair device</span>
<button class="btn-ghost w-full justify-center lg:justify-start" @click="showPair = true">
<Icon name="qrcode" :size="20" />
<span class="hidden lg:inline">Pair device</span>
</button>
<div class="mt-3 flex items-center gap-2 px-2 text-xs text-content-faint">
<div class="mt-3 hidden items-center gap-2 px-2 text-xs text-content-faint lg:flex">
<span class="h-2 w-2 rounded-full" :class="count ? 'bg-ok' : 'bg-content-faint'"></span>
{{ count }} device{{ count === 1 ? '' : 's' }} nearby
</div>
</div>
</aside>

<!-- Mobile header -->
<header class="drag-region flex items-center justify-end border-b border-white/5 bg-surface-0 px-4 py-3 md:hidden">
<header class="drag-region flex min-h-16 items-center justify-between border-b border-white/5 bg-surface-0 px-4 py-3 md:hidden">
<div class="brand-lockup">
<span class="brand-mark h-8 w-8 rounded-lg text-xs">L</span>
<div>
<div class="text-xs font-bold tracking-[0.18em] text-content">LIGHT</div>
<div class="text-[9px] uppercase tracking-[0.14em] text-content-faint">Local transfer</div>
</div>
</div>
<button class="btn-ghost" @click="showPair = true" aria-label="Pair device">
<Icon name="qrcode" />
<Icon name="qrcode" :size="20" />
</button>
</header>

<!-- Content -->
<main class="flex-1 overflow-hidden">
<main class="min-h-0 flex-1 overflow-hidden">
<slot />
</main>

<!-- Mobile bottom nav -->
<nav class="flex border-t border-white/5 bg-surface-0 pb-[env(safe-area-inset-bottom)] md:hidden">
<nav class="mobile-nav grid grid-cols-4 border-t border-white/5 bg-surface-0 md:hidden">
<RouterLink
v-for="n in nav"
:key="n.to"
:to="n.to"
class="nav-item-mobile"
active-class="nav-item-mobile--active"
>
<Icon :name="n.icon" :size="24" />
<Icon :name="n.icon" :size="20" />
<span class="truncate">{{ n.label }}</span>
</RouterLink>
</nav>
</div>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/pair/PairModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ onBeforeUnmount(() => {

<template>
<div
class="fixed inset-x-0 bottom-0 top-0 z-[1100] flex items-end justify-center bg-black/50 sm:items-center sm:p-4"
class="fixed inset-x-0 bottom-0 top-0 z-[1100] flex items-end justify-center bg-black/50 pb-[max(1rem,env(safe-area-inset-bottom))] sm:items-center sm:p-4"
:style="{ height: viewportHeight }"
@click.self="close"
>
<div class="card w-full max-w-md animate-slideUp overflow-y-auto p-5" :class="scanning ? '' : 'max-h-[85vh]'">
<div class="card w-full max-w-md animate-slideUp overflow-y-auto p-5" :class="scanning ? 'max-h-[calc(100dvh-1rem)]' : 'max-h-[85vh]'">
<div class="mb-4 flex items-center justify-between">
<h2 class="flex items-center gap-2 font-semibold">
<Icon name="qrcode" /> Pair a device
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/receive/IncomingRequest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ async function reject() {
<template>
<div
v-if="pendingReceive"
class="fixed inset-0 z-[1000] flex items-end justify-center bg-black/50 p-4 sm:grid sm:place-items-center"
class="fixed inset-0 z-[1000] flex items-end justify-center bg-black/50 p-4 pb-[max(1rem,env(safe-area-inset-bottom))] sm:grid sm:place-items-center"
@click.self="reject"
>
<div class="card w-full max-w-md animate-slideUp p-5">
<div class="card max-h-[calc(100dvh-1rem)] w-full max-w-md animate-slideUp overflow-y-auto p-5">
<div class="mb-1 flex items-center gap-2 text-accent">
<Icon name="receive" />
<span class="font-semibold">Incoming files</span>
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/transfer/FileRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ const statusLabel: Record<string, string> = {
</script>

<template>
<div class="card flex items-center gap-3 p-3">
<div class="grid h-10 w-10 place-items-center rounded-lg bg-surface-2 text-content-muted">
<div class="card grid grid-cols-[auto_minmax(0,1fr)] gap-3 p-3 sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center sm:p-3.5">
<div class="grid h-10 w-10 place-items-center rounded-lg bg-surface-2 text-content-muted sm:h-11 sm:w-11">
<Icon name="file" />
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between gap-2">
<span class="truncate font-medium">{{ transfer.filename }}</span>
<span class="shrink-0 text-xs text-content-faint">{{ statusLabel[transfer.status] || transfer.status }}</span>
<span class="shrink-0 rounded-full bg-white/5 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-content-faint">{{ statusLabel[transfer.status] || transfer.status }}</span>
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-content-faint">
<span class="font-mono">{{ formatBytes(transfer.transferred) }} / {{ formatBytes(transfer.size) }}</span>
Expand All @@ -40,7 +40,7 @@ const statusLabel: Record<string, string> = {
<ProgressBar :percent="transfer.percent" :active="transfer.status === 'active'" :failed="transfer.status === 'failed'" />
</div>
</div>
<div class="flex shrink-0 items-center gap-1">
<div class="col-start-2 flex items-center gap-1 sm:col-start-auto">
<button
v-if="transfer.status === 'active'"
class="btn-ghost px-2"
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/transfer/TransferArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ const hasTransfers = computed(() => transfers.value.length > 0)
</script>

<template>
<div class="flex h-full flex-col gap-5 p-4 sm:p-5 md:p-8">
<div class="flex h-full min-h-0 flex-col gap-4 p-4 sm:gap-5 sm:p-6 lg:p-8 xl:p-10">
<div
class="flex cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-white/10 bg-surface-0/30 px-4 py-8 text-center transition hover:border-accent/40 hover:bg-accent-soft/30 sm:px-6 sm:py-12"
class="drop-zone flex min-h-44 cursor-pointer touch-manipulation flex-col items-center justify-center rounded-2xl border-2 border-dashed border-white/10 bg-surface-0/30 px-4 py-8 text-center transition hover:border-accent/40 hover:bg-accent-soft/30 sm:min-h-52 sm:px-6 sm:py-12"
:class="dragOver ? 'border-accent bg-accent-soft' : ''"
@click="pickFiles"
@dragover.prevent="dragOver = true"
Expand All @@ -62,11 +62,11 @@ const hasTransfers = computed(() => transfers.value.length > 0)
<div class="mb-3 grid h-12 w-12 place-items-center rounded-full bg-surface-2 text-accent sm:h-14 sm:w-14">
<Icon name="upload" :size="24" />
</div>
<p class="min-w-0 font-medium">Drop files or click to browse</p>
<p class="min-w-0 text-sm text-content-faint">Sending to {{ device.name }}</p>
<p class="min-w-0 max-w-full truncate font-medium">Drop files or click to browse</p>
<p class="min-w-0 max-w-full truncate text-sm text-content-faint">Sending to {{ device.name }}</p>
</div>

<div class="flex-1 overflow-auto">
<div class="min-h-0 flex-1 overflow-y-auto pr-0.5">
<p v-if="!hasTransfers" class="grid h-full place-items-center text-sm text-content-faint">No transfers yet</p>
<div v-else class="flex flex-col gap-2">
<FileRow
Expand Down
39 changes: 34 additions & 5 deletions frontend/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
html,
body,
#app {
min-height: 100%;
height: 100%;
}

Expand All @@ -22,6 +23,11 @@
overscroll-behavior: none;
}

button,
a {
-webkit-tap-highlight-color: transparent;
}

:focus-visible {
outline: 2px solid theme('colors.accent.DEFAULT');
outline-offset: 2px;
Expand All @@ -42,7 +48,7 @@
}

.btn {
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition no-drag;
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition no-drag;
}
.btn-accent {
@apply btn bg-accent text-ink shadow-[0_8px_20px_rgba(240,165,0,0.16)] hover:brightness-110;
Expand All @@ -52,13 +58,13 @@
}

.nav-item {
@apply flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-content-muted transition no-drag hover:text-content hover:bg-white/5;
@apply flex min-h-11 items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-content-muted transition no-drag hover:bg-white/5 hover:text-content;
}
.nav-item--active {
@apply bg-accent-soft text-accent;
}
.nav-item-mobile {
@apply flex flex-1 flex-col items-center justify-center gap-1 py-2.5 text-content-faint no-drag transition-colors;
@apply flex min-w-0 flex-1 flex-col items-center justify-center gap-1 px-1 py-2 text-[10px] font-medium tracking-wide text-content-faint no-drag transition-colors;
}
.nav-item-mobile--active {
@apply text-accent;
Expand All @@ -69,11 +75,11 @@
}

.page {
@apply flex h-full flex-col overflow-auto p-4 sm:p-5 md:p-8;
@apply flex h-full min-h-0 flex-col overflow-auto px-4 pb-5 pt-5 sm:px-6 sm:py-6 lg:px-8 lg:py-8 xl:px-10;
}

.page-header {
@apply mb-6 flex items-end justify-between gap-4;
@apply mb-5 flex items-end justify-between gap-4 sm:mb-6;
}

.page-kicker {
Expand Down Expand Up @@ -107,6 +113,23 @@
.toggle-thumb--on {
@apply translate-x-5;
}

.brand-lockup {
@apply flex items-center gap-2.5;
}

.brand-mark {
@apply grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent text-sm font-black tracking-[-0.08em] text-ink shadow-[0_8px_22px_rgba(240,165,0,0.2)];
}

.mobile-nav {
padding-bottom: env(safe-area-inset-bottom);
}

.drop-zone {
background-image: linear-gradient(135deg, rgba(240, 165, 0, 0.04), transparent 48%),
radial-gradient(circle at 50% 0%, rgba(52, 211, 153, 0.08), transparent 42%);
}
}

@layer utilities {
Expand All @@ -124,3 +147,9 @@
background: rgba(255, 255, 255, 0.12);
border-radius: 999px;
}

@media (min-width: 768px) and (max-width: 1023px) {
.page {
@apply px-6 py-7;
}
}
12 changes: 6 additions & 6 deletions frontend/src/views/ReceiveView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const needsFolder = computed(

<div
v-if="needsFolder"
class="card mb-5 flex items-center gap-3 border-danger/30 bg-danger/10 p-4"
class="card mb-4 flex flex-wrap items-center gap-3 border-danger/30 bg-danger/10 p-4 sm:mb-5 sm:flex-nowrap"
>
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-danger/20 text-danger">
<Icon name="warning" />
Expand All @@ -35,26 +35,26 @@ const needsFolder = computed(
You can't receive files until you pick a download folder in Settings.
</div>
</div>
<RouterLink to="/settings" class="btn-ghost shrink-0 border border-white/10">Set folder</RouterLink>
<RouterLink to="/settings" class="btn-ghost w-full shrink-0 border border-white/10 sm:w-auto">Set folder</RouterLink>
</div>

<div class="card mb-5 flex flex-wrap items-center gap-3 p-4 md:p-5">
<div class="card mb-4 flex flex-wrap items-center gap-3 p-4 sm:mb-5 sm:flex-nowrap sm:p-5">
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-ok/15 text-ok">
<Icon name="receive" />
</div>
<div class="min-w-0 flex-1">
<div class="font-medium">Ready to receive</div>
<div class="truncate text-xs text-content-faint">
Downloads to {{ settings.downloadDir }} · Port {{ settings.port }}
Downloads to {{ settings.downloadDir }} / Port {{ settings.port }}
</div>
</div>
<span class="shrink-0 rounded-full bg-ok/20 px-2 py-0.5 text-xs text-ok">Active</span>
</div>

<p v-if="transfers.length === 0" class="grid flex-1 place-items-center text-sm text-content-faint">
<p v-if="transfers.length === 0" class="grid min-h-40 flex-1 place-items-center text-center text-sm text-content-faint">
Incoming transfers will appear here.
</p>
<div v-else class="flex flex-1 flex-col gap-2">
<div v-else class="min-h-0 flex-1 space-y-2 overflow-y-auto pr-0.5">
<FileRow
v-for="t in transfers"
:key="t.id"
Expand Down
Loading
Loading