feat(downloads): share UI — user picker + public watch links + watch page
Rework the share dialog into two clear modes and add the public /watch player page: - ShareDialog: (A) 'Share with a user' — autocomplete picker over registered users (was a blind email box that 404'd on non-users); (B) 'Share a link' — create/list/copy/revoke public links with allow-download toggle, optional expiry (1/7/30d), optional password; per-link view count. - WatchPage: standalone login-free player at /watch/<token> (routed in main.tsx like /privacy), self-contained mini-i18n (en/hu/de by browser language); password gate → unlock → play; shows a Download button only when the link allows it. - api: ShareLink/ShareRecipient types + link CRUD + recipients; share i18n (en/hu/de). Verified end-to-end in a real browser: user picker, link create, public playback, stream-only vs downloadable, password gate + unlock, no console errors.
This commit is contained in:
parent
d672583830
commit
391b8fda33
8 changed files with 543 additions and 38 deletions
|
|
@ -19,6 +19,7 @@ import Modal from "./Modal";
|
|||
import DownloadDialog from "./DownloadDialog";
|
||||
import ProfileEditor from "./ProfileEditor";
|
||||
import VideoEditor from "./VideoEditor";
|
||||
import ShareDialog from "./ShareDialog";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { api, type DownloadJob, type Me } from "../lib/api";
|
||||
|
|
@ -186,39 +187,6 @@ function RenameModal({ job, onClose }: { job: DownloadJob; onClose: () => void }
|
|||
);
|
||||
}
|
||||
|
||||
function ShareModal({ job, onClose }: { job: DownloadJob; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [email, setEmail] = useState("");
|
||||
const share = useMutation({
|
||||
mutationFn: () => api.shareDownload(job.id, email.trim()),
|
||||
onSuccess: (r) => {
|
||||
notify({ level: "success", message: t("downloads.share.done", { email: r.shared_with }) });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Modal title={t("downloads.share.title")} onClose={onClose}>
|
||||
<label className="block text-sm font-medium mb-1">{t("downloads.share.label")}</label>
|
||||
<input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={t("downloads.share.placeholder")}
|
||||
className={inputCls}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end mt-4">
|
||||
<button
|
||||
onClick={() => share.mutate()}
|
||||
disabled={!email.trim() || share.isPending}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium bg-accent text-accent-fg hover:opacity-90 disabled:opacity-50 transition"
|
||||
>
|
||||
{t("downloads.share.submit")}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Usage bar ------------------------------------------------------------------------------
|
||||
|
||||
function UsageBar() {
|
||||
|
|
@ -541,7 +509,7 @@ export default function DownloadCenter({ me }: { me: Me }) {
|
|||
)}
|
||||
{manage && <ProfileEditor onClose={() => setManage(false)} />}
|
||||
{renameJob && <RenameModal job={renameJob} onClose={() => setRenameJob(null)} />}
|
||||
{shareJob && <ShareModal job={shareJob} onClose={() => setShareJob(null)} />}
|
||||
{shareJob && <ShareDialog job={shareJob} onClose={() => setShareJob(null)} />}
|
||||
{editJob && <VideoEditor job={editJob} onClose={() => setEditJob(null)} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue