drop-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev b2e053fd98cb
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 80ddb8c5df822e290afdd3c7
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/ui.tsx
// Small shared pieces: the app bar, toast, copy and share, keeping the screen awake, and icons.
import { useEffect, useState, type ReactNode } from "react";
import { navigate } from "./lib/router";

let pushToast: ((text: string) => void) | null = null;
export function toast(text: string) { pushToast?.(text); }

export function Toaster() {
  const [text, setText] = useState<string | null>(null);
  useEffect(() => {
    let timer: number | undefined;
    pushToast = (t) => {
      setText(t);
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setText(null), 3600);
    };
    return () => { pushToast = null; window.clearTimeout(timer); };
  }, []);
  return text ? <div className="toast" role="status">{text}</div> : null;
}

export async function copy(text: string, done = "Copied") {
  try {
    await navigator.clipboard.writeText(text);
    toast(done);
  } catch {
    toast("Could not copy. Select the text and copy it.");
  }
}

/** The app screens' top bar: Drop's name (home) and the screen's own action. */
export function AppBar({ children, leave }: { children?: ReactNode; leave?: string | null }) {
  return (
    <header className="app-bar">
      <a
        className="app-brand" href="/"
        onClick={(e) => {
          e.preventDefault();
          if (leave && !confirm(leave)) return;
          navigate("/");
        }}
      >
        <img src="/logo.svg" alt="" width="24" height="24" />
        <span>Drop</span>
      </a>
      <div className="app-bar-right">{children}</div>
    </header>
  );
}

/** Asks before closing or reloading the tab while `active` (a transfer would stop). */
export function useLeaveGuard(active: boolean) {
  useEffect(() => {
    if (!active) return;
    const onLeave = (e: BeforeUnloadEvent) => { e.preventDefault(); e.returnValue = ""; };
    window.addEventListener("beforeunload", onLeave);
    return () => window.removeEventListener("beforeunload", onLeave);
  }, [active]);
}

/** Keeps the screen on while active (where the browser allows), so a phone does not sleep mid-transfer. */
export function useWakeLock(active: boolean) {
  useEffect(() => {
    if (!active || !("wakeLock" in navigator)) return;
    let lock: WakeLockSentinel | null = null;
    let cancelled = false;
    const take = async () => {
      if (document.visibilityState !== "visible" || cancelled) return;
      try { lock = await navigator.wakeLock.request("screen"); } catch { /* not allowed here */ }
    };
    const onVisible = () => { if (!lock || lock.released) void take(); };
    void take();
    document.addEventListener("visibilitychange", onVisible);
    return () => {
      cancelled = true;
      document.removeEventListener("visibilitychange", onVisible);
      void lock?.release().catch(() => null);
    };
  }, [active]);
}

export function ProgressBar({ value, label }: { value: number; label: string }) {
  const pct = Math.max(0, Math.min(100, value * 100));
  return (
    <div className="progress" role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(pct)}>
      <span style={{ width: `${pct}%` }} />
    </div>
  );
}

const PATHS: Record<string, ReactNode> = {
  send: <path d="M17.5 2.5 2.5 9l6 2.5m9-9L11 17.5 8.5 11.5m9-9-9 9" />,
  folder: <path d="M2.5 5.5a1.5 1.5 0 0 1 1.5-1.5h3.6l1.8 2H16a1.5 1.5 0 0 1 1.5 1.5v7.5A1.5 1.5 0 0 1 16 16.5H4A1.5 1.5 0 0 1 2.5 15z" />,
  file: <><path d="M5 2.5h6.5L15.5 6.5v11H5z" /><path d="M11.5 2.5v4h4" /></>,
  zip: <><path d="M5 2.5h6.5L15.5 6.5v11H5z" /><path d="M9 4.5h1.5M8 6.5h1.5M9 8.5h1.5M8 10.5h1.5M8.2 12.5h2.6v2.5H8.2z" /></>,
  link: <><path d="M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1" /><path d="M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1" /></>,
  share: <><path d="M10 12.5V2.5M6.5 6 10 2.5 13.5 6" /><path d="M6 9H4.5v8.5h11V9H14" /></>,
  download: <><path d="M10 2.5v10M6.5 9 10 12.5 13.5 9" /><path d="M3.5 13.5v3h13v-3" /></>,
  check: <path d="m4.5 10.5 3.5 3.5 7.5-8" />,
  plus: <path d="M10 4v12M4 10h12" />,
  close: <path d="M5 5l10 10M15 5 5 15" />,
  wifi: <><path d="M2.5 7.5a11 11 0 0 1 15 0M5 10.5a7.5 7.5 0 0 1 10 0M7.5 13.3a3.8 3.8 0 0 1 5 0" /><circle cx="10" cy="16" r=".9" /></>,
  lock: <><rect x="4.5" y="9" width="11" height="8" rx="1.5" /><path d="M7 9V6.5a3 3 0 0 1 6 0V9" /></>,
};

export function Icon({ name, size = 20 }: { name: keyof typeof PATHS | string; size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      {PATHS[name]}
    </svg>
  );
}

Change history

Point OpenApps links to openapps.free
b2e053fd98 · 17 hours ago
Bind agent address in policy
ca424dd814 · 19 hours ago
Retry release capture
ae07f1a3fd · 19 hours ago
Use OpenApps service URLs and add contribution strip
0452ce136f · 19 hours ago
Bounded source sync
ca1bc00aa4 · 1 day ago
Bounded source sync
d4982911a0 · 1 day ago
Bounded source sync
18c97fe4ca · 1 day ago