feat(downloads): edit + remove actions on shared-with-me items (no re-share)
A shared-with-me item only had a download button. Add two actions (Share is intentionally NOT
offered — no chain re-sharing of someone else's file):
- Edit: the editor now accepts an accessible (owned OR shared) source, so editing a shared video
produces the editor's OWN clip in their library (counts against their quota, fully theirs
including share); the source file is only read. Route uses _accessible_job.
- Remove from my list: DELETE /api/downloads/shared/{job_id} deletes only the recipient's share
grant — the owner's job and physical file are untouched (per-user dismissal).
i18n en/hu/de. Verified in a real browser (edit a shared 70-min video → own 2:31 clip; remove
confirm shows 'won't delete the owner's file').
This commit is contained in:
parent
b7f365dd5a
commit
1e3fad9a8f
6 changed files with 66 additions and 8 deletions
|
|
@ -258,8 +258,12 @@ def enqueue_download(
|
||||||
def enqueue_edit(
|
def enqueue_edit(
|
||||||
payload: dict, user: User = Depends(require_human), db: Session = Depends(get_db)
|
payload: dict, user: User = Depends(require_human), db: Session = Depends(get_db)
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Queue a phase-2 editor derivative (trim/crop) of one of the user's finished downloads."""
|
"""Queue a phase-2 editor derivative (trim/crop) of a download this user can access.
|
||||||
source_job = _own_job(db, user, int(payload.get("source_job_id") or 0))
|
|
||||||
|
The source may be the user's own download OR one shared with them — either way the resulting
|
||||||
|
clip is a NEW per-user derivative in the editor's own library (counts against their quota); the
|
||||||
|
source file is only read, never modified, so editing a shared video is safe."""
|
||||||
|
source_job = _accessible_job(db, user, int(payload.get("source_job_id") or 0))
|
||||||
if source_job.status != "done" or source_job.asset_id is None:
|
if source_job.status != "done" or source_job.asset_id is None:
|
||||||
raise HTTPException(status_code=409, detail="You can only edit a finished download.")
|
raise HTTPException(status_code=409, detail="You can only edit a finished download.")
|
||||||
spec = payload.get("edit_spec")
|
spec = payload.get("edit_spec")
|
||||||
|
|
@ -619,6 +623,24 @@ def unshare_download(
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/shared/{job_id}")
|
||||||
|
def remove_shared_with_me(
|
||||||
|
job_id: int, user: User = Depends(require_human), db: Session = Depends(get_db)
|
||||||
|
) -> dict:
|
||||||
|
"""Remove a download that was shared WITH this user from their 'Shared with me' list. Deletes
|
||||||
|
only the recipient's share grant — the owner's job and the physical file are untouched (this is
|
||||||
|
a per-user dismissal, not a delete)."""
|
||||||
|
row = db.execute(
|
||||||
|
select(DownloadShare).where(
|
||||||
|
DownloadShare.job_id == job_id, DownloadShare.to_user_id == user.id
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if row is not None:
|
||||||
|
db.delete(row)
|
||||||
|
db.commit()
|
||||||
|
return {"removed": job_id}
|
||||||
|
|
||||||
|
|
||||||
# --- recipients (for the internal "share with a user" picker) ------------------------------
|
# --- recipients (for the internal "share with a user" picker) ------------------------------
|
||||||
|
|
||||||
@router.get("/recipients")
|
@router.get("/recipients")
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,12 @@ export default function DownloadCenter({ me }: { me: Me }) {
|
||||||
onSuccess: invalidate,
|
onSuccess: invalidate,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove a shared-with-me item from your list (only your grant; the owner's file is untouched).
|
||||||
|
const removeShared = useMutation({
|
||||||
|
mutationFn: (id: number) => api.removeSharedDownload(id),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["downloads-shared"] }),
|
||||||
|
});
|
||||||
|
|
||||||
const confirmThen = async (title: string, body: string, fn: () => void) => {
|
const confirmThen = async (title: string, body: string, fn: () => void) => {
|
||||||
if (await confirm({ title, message: body, confirmLabel: title, danger: true })) fn();
|
if (await confirm({ title, message: body, confirmLabel: title, danger: true })) fn();
|
||||||
};
|
};
|
||||||
|
|
@ -545,6 +551,24 @@ export default function DownloadCenter({ me }: { me: Me }) {
|
||||||
{(sharedQ.data ?? []).map((job) => (
|
{(sharedQ.data ?? []).map((job) => (
|
||||||
<DownloadRow key={job.id} job={job}>
|
<DownloadRow key={job.id} job={job}>
|
||||||
{job.can_download && saveBtn(job)}
|
{job.can_download && saveBtn(job)}
|
||||||
|
{job.can_download && (job.asset?.duration_s ?? 0) > 0 && (
|
||||||
|
<IconBtn onClick={() => setEditJob(job)} title={t("downloads.actions.edit")}>
|
||||||
|
<Scissors className="w-4 h-4" />
|
||||||
|
</IconBtn>
|
||||||
|
)}
|
||||||
|
<IconBtn
|
||||||
|
onClick={() =>
|
||||||
|
confirmThen(
|
||||||
|
t("downloads.confirm.removeSharedTitle"),
|
||||||
|
t("downloads.confirm.removeSharedBody"),
|
||||||
|
() => removeShared.mutate(job.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={t("downloads.actions.removeShared")}
|
||||||
|
danger
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</IconBtn>
|
||||||
</DownloadRow>
|
</DownloadRow>
|
||||||
))}
|
))}
|
||||||
{sharedQ.data && sharedQ.data.length === 0 && (
|
{sharedQ.data && sharedQ.data.length === 0 && (
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"download": "Auf mein Gerät speichern",
|
"download": "Auf mein Gerät speichern",
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"edit": "Bearbeiten / schneiden"
|
"edit": "Bearbeiten / schneiden",
|
||||||
|
"removeShared": "Aus meiner Liste entfernen"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"queue": "Die Warteschlange ist leer.",
|
"queue": "Die Warteschlange ist leer.",
|
||||||
|
|
@ -110,7 +111,9 @@
|
||||||
"cancelTitle": "Download abbrechen?",
|
"cancelTitle": "Download abbrechen?",
|
||||||
"cancelBody": "Dies stoppt den Download und entfernt ihn aus deiner Warteschlange.",
|
"cancelBody": "Dies stoppt den Download und entfernt ihn aus deiner Warteschlange.",
|
||||||
"deleteTitle": "Download entfernen?",
|
"deleteTitle": "Download entfernen?",
|
||||||
"deleteBody": "Dies entfernt ihn aus deiner Liste. Die Datei kann bis zum Ablauf für andere Nutzer im Cache bleiben."
|
"deleteBody": "Dies entfernt ihn aus deiner Liste. Die Datei kann bis zum Ablauf für andere Nutzer im Cache bleiben.",
|
||||||
|
"removeSharedTitle": "Aus deiner Liste entfernen?",
|
||||||
|
"removeSharedBody": "Entfernt es nur aus deiner geteilten Liste — die Datei des Besitzers wird nicht gelöscht."
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"manage": "Formate verwalten",
|
"manage": "Formate verwalten",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"download": "Save to my device",
|
"download": "Save to my device",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"edit": "Edit / trim"
|
"edit": "Edit / trim",
|
||||||
|
"removeShared": "Remove from my list"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"queue": "Nothing in the queue.",
|
"queue": "Nothing in the queue.",
|
||||||
|
|
@ -110,7 +111,9 @@
|
||||||
"cancelTitle": "Cancel download?",
|
"cancelTitle": "Cancel download?",
|
||||||
"cancelBody": "This stops the download and removes it from your queue.",
|
"cancelBody": "This stops the download and removes it from your queue.",
|
||||||
"deleteTitle": "Remove download?",
|
"deleteTitle": "Remove download?",
|
||||||
"deleteBody": "This removes it from your list. The file may stay cached for other users until it expires."
|
"deleteBody": "This removes it from your list. The file may stay cached for other users until it expires.",
|
||||||
|
"removeSharedTitle": "Remove from your list?",
|
||||||
|
"removeSharedBody": "This removes it from your shared list only — it won't delete the owner's file."
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"manage": "Manage formats",
|
"manage": "Manage formats",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"download": "Mentés a gépemre",
|
"download": "Mentés a gépemre",
|
||||||
"rename": "Átnevezés",
|
"rename": "Átnevezés",
|
||||||
"share": "Megosztás",
|
"share": "Megosztás",
|
||||||
"edit": "Szerkesztés / vágás"
|
"edit": "Szerkesztés / vágás",
|
||||||
|
"removeShared": "Eltávolítás a listámból"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"queue": "A sor üres.",
|
"queue": "A sor üres.",
|
||||||
|
|
@ -110,7 +111,9 @@
|
||||||
"cancelTitle": "Megszakítod a letöltést?",
|
"cancelTitle": "Megszakítod a letöltést?",
|
||||||
"cancelBody": "Ez leállítja a letöltést és eltávolítja a sorból.",
|
"cancelBody": "Ez leállítja a letöltést és eltávolítja a sorból.",
|
||||||
"deleteTitle": "Eltávolítod a letöltést?",
|
"deleteTitle": "Eltávolítod a letöltést?",
|
||||||
"deleteBody": "Ez eltávolítja a listádról. A fájl a lejáratáig még a gyorsítótárban maradhat mások számára."
|
"deleteBody": "Ez eltávolítja a listádról. A fájl a lejáratáig még a gyorsítótárban maradhat mások számára.",
|
||||||
|
"removeSharedTitle": "Eltávolítod a listádból?",
|
||||||
|
"removeSharedBody": "Csak a te megosztott listádból tűnik el — a tulajdonos fájlját nem törli."
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"manage": "Formátumok kezelése",
|
"manage": "Formátumok kezelése",
|
||||||
|
|
|
||||||
|
|
@ -1075,6 +1075,9 @@ export const api = {
|
||||||
req(`/api/downloads/${id}/storyboard`),
|
req(`/api/downloads/${id}/storyboard`),
|
||||||
downloads: (): Promise<DownloadJob[]> => req("/api/downloads"),
|
downloads: (): Promise<DownloadJob[]> => req("/api/downloads"),
|
||||||
downloadsShared: (): Promise<DownloadJob[]> => req("/api/downloads/shared"),
|
downloadsShared: (): Promise<DownloadJob[]> => req("/api/downloads/shared"),
|
||||||
|
// Remove a shared-with-me item from your list (deletes only your share grant, not the file).
|
||||||
|
removeSharedDownload: (jobId: number): Promise<{ removed: number }> =>
|
||||||
|
req(`/api/downloads/shared/${jobId}`, { method: "DELETE" }),
|
||||||
downloadUsage: (): Promise<DownloadUsage> => req("/api/downloads/usage"),
|
downloadUsage: (): Promise<DownloadUsage> => req("/api/downloads/usage"),
|
||||||
downloadIndex: (): Promise<Record<string, DownloadStatus>> => req("/api/downloads/index"),
|
downloadIndex: (): Promise<Record<string, DownloadStatus>> => req("/api/downloads/index"),
|
||||||
pauseDownload: (id: number): Promise<DownloadJob> =>
|
pauseDownload: (id: number): Promise<DownloadJob> =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue