feat: add rename to history items.
This commit is contained in:
parent
80316df952
commit
285e661667
8 changed files with 384 additions and 73 deletions
38
API.md
38
API.md
|
|
@ -24,6 +24,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
||||||
- [DELETE /api/history](#delete-apihistory)
|
- [DELETE /api/history](#delete-apihistory)
|
||||||
- [POST /api/history/{id}](#post-apihistoryid)
|
- [POST /api/history/{id}](#post-apihistoryid)
|
||||||
- [GET /api/history/{id}](#get-apihistoryid)
|
- [GET /api/history/{id}](#get-apihistoryid)
|
||||||
|
- [POST /api/history/{id}/rename](#post-apihistoryidrename)
|
||||||
- [GET /api/history](#get-apihistory)
|
- [GET /api/history](#get-apihistory)
|
||||||
- [GET /api/history/live](#get-apihistorylive)
|
- [GET /api/history/live](#get-apihistorylive)
|
||||||
- [POST /api/history/start](#post-apihistorystart)
|
- [POST /api/history/start](#post-apihistorystart)
|
||||||
|
|
@ -56,7 +57,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
||||||
- [GET /api/player/segments/{segment}/{file:.\*}.ts](#get-apiplayersegmentssegmentfilets)
|
- [GET /api/player/segments/{segment}/{file:.\*}.ts](#get-apiplayersegmentssegmentfilets)
|
||||||
- [GET /api/player/subtitle/{file:.\*}.vtt](#get-apiplayersubtitlefilevtt)
|
- [GET /api/player/subtitle/{file:.\*}.vtt](#get-apiplayersubtitlefilevtt)
|
||||||
- [GET /api/player/subtitles/manifest/{file:.\*}](#get-apiplayersubtitlesmanifestfile)
|
- [GET /api/player/subtitles/manifest/{file:.\*}](#get-apiplayersubtitlesmanifestfile)
|
||||||
- [GET /api/player/subtitles/{source_format}/{file:.\*}](#get-apiplayersubtitlessource_formatfile)
|
- [GET /api/player/subtitles/{source\_format}/{file:.\*}](#get-apiplayersubtitlessource_formatfile)
|
||||||
- [GET /api/thumbnail](#get-apithumbnail)
|
- [GET /api/thumbnail](#get-apithumbnail)
|
||||||
- [GET /api/file/ffprobe/{file:.\*}](#get-apifileffprobefile)
|
- [GET /api/file/ffprobe/{file:.\*}](#get-apifileffprobefile)
|
||||||
- [GET /api/file/info/{file:.\*}](#get-apifileinfofile)
|
- [GET /api/file/info/{file:.\*}](#get-apifileinfofile)
|
||||||
|
|
@ -532,6 +533,41 @@ or an error:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### POST /api/history/{id}/rename
|
||||||
|
**Purpose**: Rename a downloaded history file and its sidecars.
|
||||||
|
|
||||||
|
**Path Parameter**:
|
||||||
|
- `id` = Unique history item ID.
|
||||||
|
|
||||||
|
**Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"new_name": "renamed_video.mp4"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "<uuid>",
|
||||||
|
"title": "Video Title",
|
||||||
|
"url": "https://youtube.com/watch?v=...",
|
||||||
|
....
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses**:
|
||||||
|
- `400 Bad Request` if `id` or `new_name` is missing, or the item has no downloaded file.
|
||||||
|
- `404 Not Found` if the item does not exist.
|
||||||
|
- `409 Conflict` if the rename destination already exists.
|
||||||
|
- `500 Internal Server Error` if the filesystem rename fails unexpectedly.
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- Uses the same file rename behavior as the file browser actions.
|
||||||
|
- Renames matching sidecar files together with the main media file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### GET /api/history
|
### GET /api/history
|
||||||
**Purpose**: Returns the download queue and/or download history with optional pagination support.
|
**Purpose**: Returns the download queue and/or download history with optional pagination support.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ class HttpAPI:
|
||||||
|
|
||||||
response: Response = await handler(request)
|
response: Response = await handler(request)
|
||||||
|
|
||||||
contentType: str = response.headers.get("content-type", "")
|
contentType: str = str(response.headers.get("content-type", ""))
|
||||||
if contentType.startswith("text/html") and getattr(response, "_path", None):
|
if contentType.startswith("text/html") and getattr(response, "_path", None):
|
||||||
rewrite_path: str = base_path.rstrip("/")
|
rewrite_path: str = base_path.rstrip("/")
|
||||||
async with await anyio.open_file(response._path, "rb") as f:
|
async with await anyio.open_file(response._path, "rb") as f:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
@ -10,10 +11,12 @@ from app.features.presets.service import Presets
|
||||||
from app.library.config import Config
|
from app.library.config import Config
|
||||||
from app.library.DataStore import StoreType
|
from app.library.DataStore import StoreType
|
||||||
from app.library.downloads import Download, DownloadQueue
|
from app.library.downloads import Download, DownloadQueue
|
||||||
|
from app.library.downloads.utils import safe_relative_path
|
||||||
from app.library.encoder import Encoder
|
from app.library.encoder import Encoder
|
||||||
from app.library.Events import EventBus, Events
|
from app.library.Events import EventBus, Events
|
||||||
from app.library.ItemDTO import Item
|
from app.library.ItemDTO import Item
|
||||||
from app.library.router import route
|
from app.library.router import route
|
||||||
|
from app.library.Utils import calc_download_path, get_file_sidecar, rename_file
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from library.downloads import Download
|
from library.downloads import Download
|
||||||
|
|
@ -287,9 +290,9 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
if "finished" == item.info.status and (filename := item.info.get_file()):
|
if "finished" == item.info.status and (filename := item.info.get_file()):
|
||||||
from app.features.streaming.library.ffprobe import ffprobe
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from app.features.streaming.library.ffprobe import ffprobe
|
||||||
|
|
||||||
info["ffprobe"] = await ffprobe(filename)
|
info["ffprobe"] = await ffprobe(filename)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -302,6 +305,70 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
|
||||||
return web.json_response(data=info, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
return web.json_response(data=info, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
||||||
|
|
||||||
|
@route("POST", r"api/history/{id}/rename", "history.item.rename")
|
||||||
|
async def item_rename(
|
||||||
|
request: Request,
|
||||||
|
queue: DownloadQueue,
|
||||||
|
encoder: Encoder,
|
||||||
|
notify: EventBus,
|
||||||
|
config: Config,
|
||||||
|
) -> Response:
|
||||||
|
if not (id := request.match_info.get("id")):
|
||||||
|
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
item: Download | None = await queue.done.get_by_id(id)
|
||||||
|
if not item:
|
||||||
|
return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code)
|
||||||
|
|
||||||
|
try:
|
||||||
|
post: dict = await request.json()
|
||||||
|
if not post:
|
||||||
|
return web.json_response(data={"error": "no data provided."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
except Exception:
|
||||||
|
return web.json_response(data={"error": "invalid JSON body."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
new_name: str = str(post.get("new_name") or "").strip()
|
||||||
|
if not new_name:
|
||||||
|
return web.json_response(data={"error": "new_name is required."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
filepath: Path | None = item.info.get_file(download_path=Path(config.download_path))
|
||||||
|
if not filepath:
|
||||||
|
return web.json_response(data={"error": "item has no downloaded file."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
try:
|
||||||
|
renamed, sidecar_renamed = rename_file(filepath, new_name)
|
||||||
|
except ValueError as e:
|
||||||
|
return web.json_response(data={"error": str(e)}, status=web.HTTPConflict.status_code)
|
||||||
|
except OSError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
|
||||||
|
|
||||||
|
item_dir: Path = (
|
||||||
|
Path(item.info.download_dir)
|
||||||
|
if item.info.download_dir
|
||||||
|
else Path(calc_download_path(base_path=config.download_path, folder=item.info.folder, create_path=False))
|
||||||
|
)
|
||||||
|
item.info.filename = safe_relative_path(renamed, item_dir, Path(config.download_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
item.info.sidecar = get_file_sidecar(renamed)
|
||||||
|
except Exception:
|
||||||
|
item.info.sidecar = {}
|
||||||
|
|
||||||
|
await queue.done.put(item, no_notify=True)
|
||||||
|
notify.emit(Events.ITEM_UPDATED, data=item.info)
|
||||||
|
|
||||||
|
sidecar_count: int = len(sidecar_renamed)
|
||||||
|
sidecar_msg: str = f" and {sidecar_count} sidecar file/s" if sidecar_count > 0 else ""
|
||||||
|
LOG.info(f"Renamed file '{filepath}' to '{renamed}'{sidecar_msg}")
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
data=item.info,
|
||||||
|
status=web.HTTPOk.status_code,
|
||||||
|
dumps=encoder.encode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@route("POST", "api/history/{id}", "item_update")
|
@route("POST", "api/history/{id}", "item_update")
|
||||||
async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder, notify: EventBus) -> Response:
|
async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.library.encoder import Encoder
|
|
||||||
from app.library.DataStore import StoreType
|
from app.library.DataStore import StoreType
|
||||||
from app.routes.api.history import items_delete
|
from app.library.ItemDTO import ItemDTO
|
||||||
|
from app.library.encoder import Encoder
|
||||||
|
from app.routes.api.history import item_rename, items_delete
|
||||||
|
from app.tests.helpers import temporary_test_dir
|
||||||
|
|
||||||
|
|
||||||
class _FakeRequest:
|
class _FakeRequest:
|
||||||
|
|
@ -14,11 +18,39 @@ class _FakeRequest:
|
||||||
self._payload = payload or {}
|
self._payload = payload or {}
|
||||||
self.query: dict[str, str] = {}
|
self.query: dict[str, str] = {}
|
||||||
self.match_info: dict[str, str] = {}
|
self.match_info: dict[str, str] = {}
|
||||||
|
self.body_exists = bool(payload)
|
||||||
|
|
||||||
async def json(self) -> dict[str, Any]:
|
async def json(self) -> dict[str, Any]:
|
||||||
return self._payload
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
def _make_download(
|
||||||
|
*,
|
||||||
|
filename: str | None = None,
|
||||||
|
folder: str = "",
|
||||||
|
download_dir: str | None = None,
|
||||||
|
status: str = "finished",
|
||||||
|
) -> SimpleNamespace:
|
||||||
|
base_dir = download_dir or "/downloads"
|
||||||
|
original_post_init = ItemDTO.__post_init__
|
||||||
|
ItemDTO.__post_init__ = lambda self: None
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = ItemDTO(
|
||||||
|
id="test-id",
|
||||||
|
title="Test Video",
|
||||||
|
url="https://example.com/watch?v=test-id",
|
||||||
|
folder=folder,
|
||||||
|
status=status,
|
||||||
|
filename=filename,
|
||||||
|
download_dir=base_dir,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
ItemDTO.__post_init__ = original_post_init
|
||||||
|
|
||||||
|
return SimpleNamespace(info=item)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_items_delete_uses_bulk_history_status_clear() -> None:
|
async def test_items_delete_uses_bulk_history_status_clear() -> None:
|
||||||
request = _FakeRequest(payload={"type": StoreType.HISTORY.value, "status": "finished,skip", "remove_file": False})
|
request = _FakeRequest(payload={"type": StoreType.HISTORY.value, "status": "finished,skip", "remove_file": False})
|
||||||
|
|
@ -47,3 +79,112 @@ async def test_items_delete_uses_bulk_history_id_clear() -> None:
|
||||||
queue.clear_bulk.assert_awaited_once_with(["a", "b"], remove_file=False)
|
queue.clear_bulk.assert_awaited_once_with(["a", "b"], remove_file=False)
|
||||||
body = json.loads(response.body.decode("utf-8"))
|
body = json.loads(response.body.decode("utf-8"))
|
||||||
assert body == {"items": {}, "deleted": 2}
|
assert body == {"items": {}, "deleted": 2}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_rename_requires_new_name() -> None:
|
||||||
|
request = _FakeRequest(payload={})
|
||||||
|
request.match_info["id"] = "item-1"
|
||||||
|
queue = SimpleNamespace(
|
||||||
|
done=SimpleNamespace(get_by_id=AsyncMock(return_value=_make_download(filename="video.mp4")))
|
||||||
|
)
|
||||||
|
encoder = Encoder()
|
||||||
|
notify = Mock()
|
||||||
|
config = SimpleNamespace(download_path="/downloads")
|
||||||
|
|
||||||
|
response = await item_rename(request, queue, encoder, notify, config)
|
||||||
|
|
||||||
|
assert response.status == 400
|
||||||
|
body = json.loads(response.body.decode("utf-8"))
|
||||||
|
assert body == {"error": "no data provided."}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_rename_returns_not_found_when_item_missing() -> None:
|
||||||
|
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
|
||||||
|
request.match_info["id"] = "missing"
|
||||||
|
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=None)))
|
||||||
|
encoder = Encoder()
|
||||||
|
notify = Mock()
|
||||||
|
config = SimpleNamespace(download_path="/downloads")
|
||||||
|
|
||||||
|
response = await item_rename(request, queue, encoder, notify, config)
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
body = json.loads(response.body.decode("utf-8"))
|
||||||
|
assert body == {"error": "item 'missing' not found."}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_rename_requires_existing_downloaded_file() -> None:
|
||||||
|
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
|
||||||
|
request.match_info["id"] = "item-1"
|
||||||
|
item = _make_download(filename="video.mp4")
|
||||||
|
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=item)))
|
||||||
|
encoder = Encoder()
|
||||||
|
notify = Mock()
|
||||||
|
config = SimpleNamespace(download_path="/downloads")
|
||||||
|
|
||||||
|
item.info.get_file = lambda download_path=None: None
|
||||||
|
|
||||||
|
response = await item_rename(request, queue, encoder, notify, config)
|
||||||
|
|
||||||
|
assert response.status == 400
|
||||||
|
body = json.loads(response.body.decode("utf-8"))
|
||||||
|
assert body == {"error": "item has no downloaded file."}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_rename_renames_file_and_sidecars() -> None:
|
||||||
|
with temporary_test_dir("history-rename") as temp_dir:
|
||||||
|
media = temp_dir / "video.mp4"
|
||||||
|
subtitle = temp_dir / "video.en.srt"
|
||||||
|
media.write_text("video")
|
||||||
|
subtitle.write_text("subtitle")
|
||||||
|
|
||||||
|
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
|
||||||
|
request.match_info["id"] = "item-1"
|
||||||
|
item = _make_download(filename="video.mp4", download_dir=str(temp_dir))
|
||||||
|
item.info._id = "item-1"
|
||||||
|
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=item), put=AsyncMock()))
|
||||||
|
encoder = Encoder()
|
||||||
|
notify = Mock()
|
||||||
|
config = SimpleNamespace(download_path=str(temp_dir))
|
||||||
|
|
||||||
|
response = await item_rename(request, queue, encoder, notify, config)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
body = json.loads(response.body.decode("utf-8"))
|
||||||
|
assert body["filename"] == "renamed.mp4"
|
||||||
|
assert item.info.filename == "renamed.mp4"
|
||||||
|
assert (temp_dir / "renamed.mp4").exists()
|
||||||
|
assert (temp_dir / "renamed.en.srt").exists()
|
||||||
|
assert not media.exists()
|
||||||
|
assert not subtitle.exists()
|
||||||
|
queue.done.put.assert_awaited_once_with(item, no_notify=True)
|
||||||
|
notify.emit.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_rename_returns_conflict_on_collision() -> None:
|
||||||
|
with temporary_test_dir("history-rename-conflict") as temp_dir:
|
||||||
|
media = temp_dir / "video.mp4"
|
||||||
|
conflict = temp_dir / "renamed.mp4"
|
||||||
|
media.write_text("video")
|
||||||
|
conflict.write_text("existing")
|
||||||
|
|
||||||
|
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
|
||||||
|
request.match_info["id"] = "item-1"
|
||||||
|
item = _make_download(filename="video.mp4", download_dir=str(temp_dir))
|
||||||
|
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=item), put=AsyncMock()))
|
||||||
|
encoder = Encoder()
|
||||||
|
notify = Mock()
|
||||||
|
config = SimpleNamespace(download_path=str(temp_dir))
|
||||||
|
|
||||||
|
response = await item_rename(request, queue, encoder, notify, config)
|
||||||
|
|
||||||
|
assert response.status == 409
|
||||||
|
body = json.loads(response.body.decode("utf-8"))
|
||||||
|
assert body == {"error": "Destination 'renamed.mp4' already exists"}
|
||||||
|
queue.done.put.assert_not_awaited()
|
||||||
|
notify.emit.assert_not_called()
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
:loading="isRefreshing"
|
:loading="isRefreshing"
|
||||||
:disabled="isRefreshing"
|
:disabled="isRefreshing"
|
||||||
:square="isMobile"
|
:square="isMobile"
|
||||||
@click="() => void refreshQueue()"
|
@click="() => refreshQueue()"
|
||||||
>
|
>
|
||||||
<span v-if="!isMobile">Reload Queue</span>
|
<span v-if="!isMobile">Reload Queue</span>
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -329,7 +329,7 @@
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-play-circle"
|
icon="i-lucide-play-circle"
|
||||||
@click="void stateStore.startItems([item._id])"
|
@click="() => stateStore.startItems([item._id])"
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -340,7 +340,7 @@
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-pause"
|
icon="i-lucide-pause"
|
||||||
@click="void stateStore.pauseItems([item._id])"
|
@click="() => stateStore.pauseItems([item._id])"
|
||||||
>
|
>
|
||||||
Pause
|
Pause
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -350,7 +350,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-x"
|
icon="i-lucide-x"
|
||||||
@click="void stateStore.cancelItems([item._id])"
|
@click="() => stateStore.cancelItems([item._id])"
|
||||||
>
|
>
|
||||||
{{ item.is_live ? 'Stop' : 'Cancel' }}
|
{{ item.is_live ? 'Stop' : 'Cancel' }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -400,7 +400,7 @@
|
||||||
:sibling-count="0"
|
:sibling-count="0"
|
||||||
@update:page="
|
@update:page="
|
||||||
(page) =>
|
(page) =>
|
||||||
loadHistory(page, {
|
load(page, {
|
||||||
order: 'DESC',
|
order: 'DESC',
|
||||||
perPage: configStore.app.default_pagination,
|
perPage: configStore.app.default_pagination,
|
||||||
})
|
})
|
||||||
|
|
@ -415,7 +415,7 @@
|
||||||
:loading="historyIsLoading"
|
:loading="historyIsLoading"
|
||||||
:disabled="historyIsLoading"
|
:disabled="historyIsLoading"
|
||||||
@click="
|
@click="
|
||||||
void reloadHistory({ order: 'DESC', perPage: configStore.app.default_pagination })
|
() => reload({ order: 'DESC', perPage: configStore.app.default_pagination })
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Reload
|
Reload
|
||||||
|
|
@ -542,7 +542,7 @@
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-rotate-cw"
|
icon="i-lucide-rotate-cw"
|
||||||
@click="() => void requeueItem(item)"
|
@click="() => requeueItem(item)"
|
||||||
>
|
>
|
||||||
Requeue
|
Requeue
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -552,7 +552,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-trash"
|
icon="i-lucide-trash"
|
||||||
@click="() => void deleteHistoryItem(item)"
|
@click="() => deleteHistoryItem(item)"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -589,7 +589,7 @@
|
||||||
:sibling-count="0"
|
:sibling-count="0"
|
||||||
@update:page="
|
@update:page="
|
||||||
(page) =>
|
(page) =>
|
||||||
loadHistory(page, {
|
load(page, {
|
||||||
order: 'DESC',
|
order: 'DESC',
|
||||||
perPage: configStore.app.default_pagination,
|
perPage: configStore.app.default_pagination,
|
||||||
})
|
})
|
||||||
|
|
@ -711,10 +711,10 @@ const {
|
||||||
items: historyItems,
|
items: historyItems,
|
||||||
pagination,
|
pagination,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadHistory,
|
load,
|
||||||
reloadHistory,
|
reload,
|
||||||
deleteHistoryItems,
|
remove,
|
||||||
historyMoveHandler,
|
moveHandler,
|
||||||
} = useHistoryState();
|
} = useHistoryState();
|
||||||
|
|
||||||
const embedUrl = ref('');
|
const embedUrl = ref('');
|
||||||
|
|
@ -1261,20 +1261,18 @@ const requeueItem = async (item: StoreItem): Promise<void> => {
|
||||||
payload.extras = JSON.parse(JSON.stringify(item.extras));
|
payload.extras = JSON.parse(JSON.stringify(item.extras));
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteHistoryItems({ ids: [item._id], removeFile: false });
|
await remove({ ids: [item._id], removeFile: false });
|
||||||
await reloadHistory({ order: 'DESC', perPage: configStore.app.default_pagination });
|
await reload({ order: 'DESC', perPage: configStore.app.default_pagination });
|
||||||
await stateStore.addDownload(payload);
|
await stateStore.addDownload(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteHistoryItem = async (item: StoreItem): Promise<void> => {
|
const deleteHistoryItem = async (item: StoreItem): Promise<void> => {
|
||||||
await deleteHistoryItems({ ids: [item._id], removeFile: app.value.remove_files });
|
await remove({ ids: [item._id], removeFile: app.value.remove_files });
|
||||||
await reloadHistory({ order: 'DESC', perPage: configStore.app.default_pagination });
|
await reload({ order: 'DESC', perPage: configStore.app.default_pagination });
|
||||||
toast.info('Removed from history queue.');
|
toast.info('Removed from history queue.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHistoryItemMoved = historyMoveHandler(
|
const handleHistoryItemMoved = moveHandler(() => simpleMode.value && historyInitialized.value);
|
||||||
() => simpleMode.value && historyInitialized.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const showMessage = (item: StoreItem): boolean => {
|
const showMessage = (item: StoreItem): boolean => {
|
||||||
if (!item?.msg || item.msg === item?.error) {
|
if (!item?.msg || item.msg === item?.error) {
|
||||||
|
|
@ -1303,7 +1301,7 @@ onMounted(async () => {
|
||||||
await normalizeSimpleRoute();
|
await normalizeSimpleRoute();
|
||||||
historyInitialized.value = true;
|
historyInitialized.value = true;
|
||||||
socketStore.on('item_moved', handleHistoryItemMoved);
|
socketStore.on('item_moved', handleHistoryItemMoved);
|
||||||
await loadHistory(1, { order: 'DESC', perPage: configStore.app.default_pagination });
|
await load(1, { order: 'DESC', perPage: configStore.app.default_pagination });
|
||||||
|
|
||||||
if (!socketStore.isConnected && autoRefreshEnabled.value) {
|
if (!socketStore.isConnected && autoRefreshEnabled.value) {
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
|
|
@ -1389,7 +1387,7 @@ watch(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadHistory(1, { order: 'DESC', perPage: configStore.app.default_pagination });
|
void load(1, { order: 'DESC', perPage: configStore.app.default_pagination });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const buildQuery = (
|
||||||
return params.toString();
|
return params.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadHistory = async (page: number = 1, options: HistoryLoadOptions = {}): Promise<void> => {
|
const load = async (page: number = 1, options: HistoryLoadOptions = {}): Promise<void> => {
|
||||||
const { order = 'DESC', status, perPage = pageSize.value } = options;
|
const { order = 'DESC', status, perPage = pageSize.value } = options;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
@ -104,12 +104,12 @@ const loadHistory = async (page: number = 1, options: HistoryLoadOptions = {}):
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadHistory = async (options: HistoryLoadOptions = {}): Promise<void> => {
|
const reload = async (options: HistoryLoadOptions = {}): Promise<void> => {
|
||||||
const targetPage = isLoaded.value ? pagination.value.page : 1;
|
const targetPage = isLoaded.value ? pagination.value.page : 1;
|
||||||
await loadHistory(targetPage, options);
|
await load(targetPage, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteHistoryItems = async (
|
const remove = async (
|
||||||
options: {
|
options: {
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
@ -155,7 +155,39 @@ const deleteHistoryItems = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetHistory = (): void => {
|
const rename = async (item: StoreItem, newName: string): Promise<boolean> => {
|
||||||
|
const trimmedName = newName.trim();
|
||||||
|
if (!trimmedName || trimmedName === item.filename?.split('/').pop()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(`/api/history/${item._id}/rename`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ new_name: trimmedName }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureSuccess(response);
|
||||||
|
|
||||||
|
const updated = (await response.json()) as StoreItem;
|
||||||
|
const index = items.value.findIndex((entry) => entry._id === updated._id);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
items.value = [...items.value.slice(0, index), updated, ...items.value.slice(index + 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
lastError.value = null;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
if (throwInstead.value) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = (): void => {
|
||||||
items.value = [];
|
items.value = [];
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
page: 1,
|
page: 1,
|
||||||
|
|
@ -169,7 +201,7 @@ const resetHistory = (): void => {
|
||||||
lastError.value = null;
|
lastError.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addHistoryItem = (item: StoreItem): void => {
|
const upsert = (item: StoreItem): void => {
|
||||||
const existingIndex = items.value.findIndex((existing) => existing._id === item._id);
|
const existingIndex = items.value.findIndex((existing) => existing._id === item._id);
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
|
|
@ -195,7 +227,7 @@ const addHistoryItem = (item: StoreItem): void => {
|
||||||
pagination.value.has_next = pagination.value.page < pagination.value.total_pages;
|
pagination.value.has_next = pagination.value.page < pagination.value.total_pages;
|
||||||
};
|
};
|
||||||
|
|
||||||
const historyMoveHandler = (
|
const moveHandler = (
|
||||||
shouldHandle: () => boolean = () => isLoaded.value,
|
shouldHandle: () => boolean = () => isLoaded.value,
|
||||||
): ((payload: WSEP['item_moved']) => void) => {
|
): ((payload: WSEP['item_moved']) => void) => {
|
||||||
return (payload: WSEP['item_moved']): void => {
|
return (payload: WSEP['item_moved']): void => {
|
||||||
|
|
@ -203,7 +235,7 @@ const historyMoveHandler = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addHistoryItem(payload.data.item);
|
upsert(payload.data.item);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -215,11 +247,12 @@ export const useHistoryState = () => {
|
||||||
isLoaded,
|
isLoaded,
|
||||||
lastError,
|
lastError,
|
||||||
pageSize,
|
pageSize,
|
||||||
loadHistory,
|
load,
|
||||||
reloadHistory,
|
reload,
|
||||||
deleteHistoryItems,
|
remove,
|
||||||
resetHistory,
|
rename,
|
||||||
upsertHistoryItem: addHistoryItem,
|
reset,
|
||||||
historyMoveHandler,
|
upsert,
|
||||||
|
moveHandler,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
icon="i-lucide-refresh-cw"
|
icon="i-lucide-refresh-cw"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
@click="void reloadHistory({ order: 'DESC', perPage: config.app.default_pagination })"
|
@click="() => reload({ order: 'DESC', perPage: config.app.default_pagination })"
|
||||||
>
|
>
|
||||||
<span>Reload</span>
|
<span>Reload</span>
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
show-edges
|
show-edges
|
||||||
:sibling-count="0"
|
:sibling-count="0"
|
||||||
@update:page="
|
@update:page="
|
||||||
(page) => loadHistory(page, { order: 'DESC', perPage: config.app.default_pagination })
|
(page) => load(page, { order: 'DESC', perPage: config.app.default_pagination })
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -314,7 +314,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-rotate-cw"
|
icon="i-lucide-rotate-cw"
|
||||||
@click="void retryItem(item, true)"
|
@click="() => retryItem(item, true)"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -337,7 +337,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="i-lucide-trash"
|
icon="i-lucide-trash"
|
||||||
@click="void removeItem(item)"
|
@click="() => removeItem(item)"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -608,7 +608,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-lucide-rotate-cw"
|
icon="i-lucide-rotate-cw"
|
||||||
class="w-full justify-center"
|
class="w-full justify-center"
|
||||||
@click="void retryItem(item, false)"
|
@click="() => retryItem(item, false)"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -631,7 +631,7 @@
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-lucide-trash"
|
icon="i-lucide-trash"
|
||||||
class="w-full justify-center"
|
class="w-full justify-center"
|
||||||
@click="void removeItem(item)"
|
@click="() => removeItem(item)"
|
||||||
>
|
>
|
||||||
{{ config.app.remove_files ? 'Remove' : 'Clear' }}
|
{{ config.app.remove_files ? 'Remove' : 'Clear' }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -703,7 +703,7 @@
|
||||||
show-edges
|
show-edges
|
||||||
:sibling-count="0"
|
:sibling-count="0"
|
||||||
@update:page="
|
@update:page="
|
||||||
(page) => loadHistory(page, { order: 'DESC', perPage: config.app.default_pagination })
|
(page) => load(page, { order: 'DESC', perPage: config.app.default_pagination })
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -786,7 +786,7 @@ const stateStore = useQueueState();
|
||||||
const socketStore = useAppSocket();
|
const socketStore = useAppSocket();
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const box = useConfirm();
|
const box = useConfirm();
|
||||||
const { confirmDialog } = useDialog();
|
const { confirmDialog, promptDialog } = useDialog();
|
||||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||||
const pendingDownloadFormItem = useState<item_request | Record<string, never>>(
|
const pendingDownloadFormItem = useState<item_request | Record<string, never>>(
|
||||||
'pending-download-form-item',
|
'pending-download-form-item',
|
||||||
|
|
@ -797,10 +797,11 @@ const {
|
||||||
pagination,
|
pagination,
|
||||||
isLoading,
|
isLoading,
|
||||||
isLoaded,
|
isLoaded,
|
||||||
loadHistory,
|
load,
|
||||||
reloadHistory,
|
reload,
|
||||||
deleteHistoryItems,
|
remove,
|
||||||
historyMoveHandler,
|
rename,
|
||||||
|
moveHandler,
|
||||||
} = useHistoryState();
|
} = useHistoryState();
|
||||||
|
|
||||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true);
|
const show_thumbnail = useStorage<boolean>('show_thumbnail', true);
|
||||||
|
|
@ -837,11 +838,11 @@ const paginationInfo = computed(() => ({
|
||||||
isLoaded: isLoaded.value,
|
isLoaded: isLoaded.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleHistoryItemMoved = historyMoveHandler();
|
const handleHistoryItemMoved = moveHandler();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
socketStore.on('item_moved', handleHistoryItemMoved);
|
socketStore.on('item_moved', handleHistoryItemMoved);
|
||||||
await loadHistory(1, { order: 'DESC', perPage: config.app.default_pagination });
|
await load(1, { order: 'DESC', perPage: config.app.default_pagination });
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
@ -1048,6 +1049,12 @@ const itemActionGroups = (item: StoreItem): Array<Array<Record<string, unknown>>
|
||||||
icon: 'i-lucide-file-code-2',
|
icon: 'i-lucide-file-code-2',
|
||||||
onSelect: () => void generateNfo(item),
|
onSelect: () => void generateNfo(item),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mediaActions.push({
|
||||||
|
label: 'Rename file',
|
||||||
|
icon: 'i-lucide-pencil',
|
||||||
|
onSelect: () => void renameFile(item),
|
||||||
|
});
|
||||||
} else if (isEmbedable(item.url)) {
|
} else if (isEmbedable(item.url)) {
|
||||||
mediaActions.push({
|
mediaActions.push({
|
||||||
label: 'Play video',
|
label: 'Play video',
|
||||||
|
|
@ -1127,9 +1134,9 @@ const deleteSelectedItems = async (): Promise<void> => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteHistoryItems({ ids: [...selectedElms.value], removeFile: config.app.remove_files });
|
await remove({ ids: [...selectedElms.value], removeFile: config.app.remove_files });
|
||||||
selectedElms.value = [];
|
selectedElms.value = [];
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearCompleted = async (): Promise<void> => {
|
const clearCompleted = async (): Promise<void> => {
|
||||||
|
|
@ -1141,9 +1148,9 @@ const clearCompleted = async (): Promise<void> => {
|
||||||
|
|
||||||
selectedElms.value = [];
|
selectedElms.value = [];
|
||||||
|
|
||||||
await deleteHistoryItems({ status: 'finished,skip', removeFile: false });
|
await remove({ status: 'finished,skip', removeFile: false });
|
||||||
|
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearIncomplete = async (): Promise<void> => {
|
const clearIncomplete = async (): Promise<void> => {
|
||||||
|
|
@ -1152,8 +1159,8 @@ const clearIncomplete = async (): Promise<void> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedElms.value = [];
|
selectedElms.value = [];
|
||||||
await deleteHistoryItems({ status: '!finished', removeFile: false });
|
await remove({ status: '!finished', removeFile: false });
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
};
|
};
|
||||||
|
|
||||||
const setIcon = (item: StoreItem): string => {
|
const setIcon = (item: StoreItem): string => {
|
||||||
|
|
@ -1307,10 +1314,10 @@ const archiveItem = async (item: StoreItem, opts = {}): Promise<void> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((opts as { remove_history?: boolean })?.remove_history) {
|
if ((opts as { remove_history?: boolean })?.remove_history) {
|
||||||
await deleteHistoryItems({ ids: [item._id], removeFile: false });
|
await remove({ ids: [item._id], removeFile: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeItem = async (item: StoreItem): Promise<void> => {
|
const removeItem = async (item: StoreItem): Promise<void> => {
|
||||||
|
|
@ -1324,13 +1331,13 @@ const removeItem = async (item: StoreItem): Promise<void> => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteHistoryItems({ ids: [item._id], removeFile: config.app.remove_files });
|
await remove({ ids: [item._id], removeFile: config.app.remove_files });
|
||||||
|
|
||||||
if (selectedElms.value.includes(item._id || '')) {
|
if (selectedElms.value.includes(item._id || '')) {
|
||||||
selectedElms.value = selectedElms.value.filter((entry) => entry !== item._id);
|
selectedElms.value = selectedElms.value.filter((entry) => entry !== item._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
};
|
};
|
||||||
|
|
||||||
const retryItem = async (
|
const retryItem = async (
|
||||||
|
|
@ -1349,13 +1356,13 @@ const retryItem = async (
|
||||||
auto_start: item.auto_start,
|
auto_start: item.auto_start,
|
||||||
};
|
};
|
||||||
|
|
||||||
await deleteHistoryItems({ ids: [item._id], removeFile: remove_file });
|
await remove({ ids: [item._id], removeFile: remove_file });
|
||||||
|
|
||||||
if (selectedElms.value.includes(item._id || '')) {
|
if (selectedElms.value.includes(item._id || '')) {
|
||||||
selectedElms.value = selectedElms.value.filter((entry) => entry !== item._id);
|
selectedElms.value = selectedElms.value.filter((entry) => entry !== item._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
|
|
||||||
if (true === re_add) {
|
if (true === re_add) {
|
||||||
toast.info('Cleared the item from history, and added it to the new download form.');
|
toast.info('Cleared the item from history, and added it to the new download form.');
|
||||||
|
|
@ -1532,10 +1539,10 @@ const removeFromArchive = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts?.remove_history) {
|
if (opts?.remove_history) {
|
||||||
await deleteHistoryItems({ ids: [item._id], removeFile: file_delete });
|
await remove({ ids: [item._id], removeFile: file_delete });
|
||||||
}
|
}
|
||||||
|
|
||||||
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
|
await reload({ order: 'DESC', perPage: config.app.default_pagination });
|
||||||
};
|
};
|
||||||
|
|
||||||
const isQueuedAnimation = (item: StoreItem): string => {
|
const isQueuedAnimation = (item: StoreItem): string => {
|
||||||
|
|
@ -1565,4 +1572,33 @@ const generateNfo = async (item: StoreItem): Promise<void> => {
|
||||||
toast.error(`Error: ${error.message}`);
|
toast.error(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renameFile = async (item: StoreItem): Promise<void> => {
|
||||||
|
if (!item.filename) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentName = item.filename.split('/').pop() || item.filename;
|
||||||
|
const { status, value: newName } = await promptDialog({
|
||||||
|
title: 'Rename File',
|
||||||
|
message: `Enter new name for '${currentName}' (and its sidecars):`,
|
||||||
|
initial: currentName,
|
||||||
|
confirmText: 'Rename',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedName = (newName || '').trim();
|
||||||
|
if (!trimmedName || trimmedName === currentName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await rename(item, trimmedName);
|
||||||
|
if (success) {
|
||||||
|
toast.success(`Renamed '${currentName}'.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
6
uv.lock
6
uv.lock
|
|
@ -1405,11 +1405,11 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2026.1.post1"
|
version = "2026.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue