diff --git a/src/lang/en/global.json b/src/lang/en/global.json
index bf5bf1bec1..d2277354d1 100644
--- a/src/lang/en/global.json
+++ b/src/lang/en/global.json
@@ -32,5 +32,12 @@
"name": "Name",
"refresh_success": "Refresh successfully",
"refresh_failed": "Refresh failed",
- "required": "required"
+ "required": "required",
+ "generate_strm": "Generate Strm",
+ "generate_strm_start": "Generation started",
+ "generate_strm_progress": "Generating Strm...",
+ "generate_strm_done": "Strm generated",
+ "generate_strm_failed": "Generation failed",
+ "generate_strm_hide": "Hide",
+ "generate_strm_background": "The task keeps running in the background. Track it in the task center."
}
diff --git a/src/lang/en/home.json b/src/lang/en/home.json
index fbaf844bd9..55793aae26 100644
--- a/src/lang/en/home.json
+++ b/src/lang/en/home.json
@@ -158,6 +158,7 @@
"new_file": "New File",
"input_filename": "Input filename",
"cancel_select": "Cancel Select",
+ "generate_strm": "Generate Strm",
"offline_download": "Offline download",
"offline_download-tips": "One URL per line",
"delete_policy": {
diff --git a/src/lang/en/manage.json b/src/lang/en/manage.json
index d5742af281..cec4ae0e5b 100644
--- a/src/lang/en/manage.json
+++ b/src/lang/en/manage.json
@@ -26,6 +26,7 @@
"permissions_config": "Config",
"upload": "Upload",
"copy": "Copy",
+ "strm_generate": "Strm Generate",
"decompress": "Decompress",
"s3_transition": "S3 Transition",
"backup-restore": "Backup & Restore",
diff --git a/src/lang/en/tasks.json b/src/lang/en/tasks.json
index 62818e03c2..85199714ce 100644
--- a/src/lang/en/tasks.json
+++ b/src/lang/en/tasks.json
@@ -3,6 +3,7 @@
"offline_download_transfer": "Transfer downloaded file to corresponding storage",
"upload": "Upload file to corresponding storage",
"copy": "Copy file from a storage to another storage",
+ "strm_generate": "Generate local .strm files for a Strm storage",
"decompress": "Download and decompress an archive file",
"decompress_upload": "Upload extracted file into target storage",
"s3_transition": "Archive and restore S3 objects",
@@ -45,6 +46,9 @@
"upload": {
"path": "Path"
},
+ "strm_generate": {
+ "path": "Path"
+ },
"offline_download": {
"url": "URL",
"path": "Destination Path",
diff --git a/src/pages/home/toolbar/Right.tsx b/src/pages/home/toolbar/Right.tsx
index 1eb5dbc1fb..4fc58f0211 100644
--- a/src/pages/home/toolbar/Right.tsx
+++ b/src/pages/home/toolbar/Right.tsx
@@ -13,6 +13,7 @@ import { usePath } from "~/hooks"
import { Motion } from "@motionone/solid"
import { isTocVisible, setTocDisabled } from "~/components"
import { BiSolidBookContent } from "solid-icons/bi"
+import { ToolbarStrmGenerate } from "./StrmGenerate"
export const Right = () => {
const { isOpen, onToggle } = createDisclosure({
@@ -122,6 +123,7 @@ export const Right = () => {
}}
/>
+
{
+ const { pathname } = useRouter()
+ const isAdmin = () => (me().role || []).includes(2)
+ const isStrm = () => objStore.provider === "Strm"
+ return (
+
+
+ {({ start }) => (
+
+ )}
+
+
+ )
+}
diff --git a/src/pages/manage/sidemenu_items.tsx b/src/pages/manage/sidemenu_items.tsx
index 940f19b714..c810ba71e5 100644
--- a/src/pages/manage/sidemenu_items.tsx
+++ b/src/pages/manage/sidemenu_items.tsx
@@ -24,7 +24,7 @@ import { Component, lazy } from "solid-js"
import { joinBase } from "~/utils"
import { Group, UserRole } from "~/types"
import { FaSolidBook, FaSolidDatabase } from "solid-icons/fa"
-import { TbArchive, TbDevices2 } from "solid-icons/tb"
+import { TbArchive, TbDevices2, TbFileExport } from "solid-icons/tb"
import { TbLink } from "solid-icons/tb"
import { FaSolidUserGear } from "solid-icons/fa"
import { BiRegularMessageAltDetail } from "solid-icons/bi"
@@ -180,6 +180,13 @@ export const side_menu_items: SideMenuItem[] = [
role: UserRole.GENERAL,
component: lazy(() => import("./tasks/Copy")),
},
+ {
+ title: "manage.sidemenu.strm_generate",
+ icon: TbFileExport,
+ to: "/@manage/tasks/strm_generate",
+ role: UserRole.ADMIN,
+ component: lazy(() => import("./tasks/StrmGenerate")),
+ },
{
title: "manage.sidemenu.decompress",
icon: TbArchive,
diff --git a/src/pages/manage/storages/Storage.tsx b/src/pages/manage/storages/Storage.tsx
index 5cbfc7685a..ad7c2d6a43 100644
--- a/src/pages/manage/storages/Storage.tsx
+++ b/src/pages/manage/storages/Storage.tsx
@@ -9,11 +9,13 @@ import {
useColorModeValue,
VStack,
} from "@hope-ui/solid"
+import { Show } from "solid-js"
import { useFetch, useRouter, useT } from "~/hooks"
import { getMainColor } from "~/store"
import { PEmptyResp, Storage } from "~/types"
import { handleResp, handleRespWithNotifySuccess, notify, r } from "~/utils"
import { DeletePopover } from "../common/DeletePopover"
+import { StrmGenerateButton } from "./StrmGenerate"
interface StorageProps {
storage: Storage
@@ -66,6 +68,9 @@ function StorageOp(props: StorageProps) {
})
}}
/>
+
+
+
>
)
}
diff --git a/src/pages/manage/storages/StrmGenerate.tsx b/src/pages/manage/storages/StrmGenerate.tsx
new file mode 100644
index 0000000000..7110995c34
--- /dev/null
+++ b/src/pages/manage/storages/StrmGenerate.tsx
@@ -0,0 +1,186 @@
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Progress,
+ ProgressIndicator,
+ Text,
+ VStack,
+} from "@hope-ui/solid"
+import { createSignal, JSX, onCleanup, Show } from "solid-js"
+import { useT } from "~/hooks"
+import { r, notify, handleResp } from "~/utils"
+
+type TaskInfo = { id: string; progress: number; state: number; error: string }
+type Status = "idle" | "running" | "done" | "failed"
+
+export type StrmGenerateProps = {
+ path: string
+ // render-prop trigger: receives `start` to kick off generation. Lets callers
+ // render either a Button (storages page) or a toolbar icon while sharing the
+ // progress dialog + polling logic below.
+ children: (api: { start: () => void }) => JSX.Element
+}
+
+export type StrmGenerateButtonProps = {
+ path: string
+ size?: "xs" | "sm" | "md" | "lg"
+ colorScheme?: string
+}
+
+export const StrmGenerate = (props: StrmGenerateProps) => {
+ const t = useT()
+ const [opened, setOpened] = createSignal(false)
+ const [progress, setProgress] = createSignal(0)
+ const [status, setStatus] = createSignal("idle")
+ const [err, setErr] = createSignal("")
+ let timer: ReturnType | undefined
+ // generation increments on every start/close/cleanup so any in-flight request
+ // or interval tick that resolves late can detect it is stale and bail out.
+ let generation = 0
+
+ const stop = () => {
+ if (timer) {
+ clearInterval(timer)
+ timer = undefined
+ }
+ }
+ onCleanup(() => {
+ generation++
+ stop()
+ })
+
+ const fail = (msg: string) => {
+ stop()
+ setStatus("failed")
+ setErr(msg)
+ }
+
+ const poll = (gen: number, tid: string) => {
+ stop()
+ timer = setInterval(async () => {
+ const resp = await r.post(`/admin/task/strm_generate/info?tid=${tid}`)
+ if (gen !== generation) return
+ handleResp(
+ resp,
+ (info: TaskInfo) => {
+ if (gen !== generation) return
+ if (info.error) {
+ fail(info.error)
+ return
+ }
+ setProgress(Math.floor(info.progress))
+ if (info.progress >= 100) {
+ stop()
+ setStatus("done")
+ notify.success(t("global.generate_strm_done"))
+ }
+ },
+ (msg: string) => {
+ if (gen === generation) fail(msg)
+ },
+ )
+ }, 1000)
+ }
+
+ const start = async () => {
+ stop()
+ const gen = ++generation
+ setOpened(true)
+ setStatus("running")
+ setProgress(0)
+ setErr("")
+ const resp = await r.post("/admin/strm/generate", { path: props.path })
+ if (gen !== generation) return
+ handleResp(
+ resp,
+ (data: { task: TaskInfo }) => {
+ if (gen !== generation) return
+ notify.info(t("global.generate_strm_start"))
+ poll(gen, data.task.id)
+ },
+ (msg: string) => {
+ if (gen === generation) fail(msg)
+ },
+ )
+ }
+
+ // hide just closes the foreground window; the backend task keeps running and
+ // polling continues, so the success toast still fires and progress stays
+ // visible in the task center.
+ const hide = () => setOpened(false)
+
+ // close is used once the task reached a terminal state: stop polling and reset.
+ const close = () => {
+ generation++
+ stop()
+ setOpened(false)
+ }
+
+ const isRunning = () => status() === "running"
+ const dismiss = () => (isRunning() ? hide() : close())
+
+ const statusText = () => {
+ switch (status()) {
+ case "failed":
+ return t("global.generate_strm_failed") + ": " + err()
+ case "done":
+ return t("global.generate_strm_done")
+ case "running":
+ return t("global.generate_strm_progress") + " " + progress() + "%"
+ default:
+ return ""
+ }
+ }
+
+ return (
+ <>
+ {props.children({ start })}
+
+
+
+ {t("global.generate_strm")}
+
+
+
+ {statusText()}
+
+
+ {t("global.generate_strm_background")}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export const StrmGenerateButton = (props: StrmGenerateButtonProps) => {
+ const t = useT()
+ return (
+
+ {({ start }) => (
+
+ )}
+
+ )
+}
diff --git a/src/pages/manage/tasks/StrmGenerate.tsx b/src/pages/manage/tasks/StrmGenerate.tsx
new file mode 100644
index 0000000000..430e8b4340
--- /dev/null
+++ b/src/pages/manage/tasks/StrmGenerate.tsx
@@ -0,0 +1,23 @@
+import { useManageTitle, useT } from "~/hooks"
+import { TypeTasks } from "./Tasks"
+
+const StrmGenerate = () => {
+ const t = useT()
+ useManageTitle("manage.sidemenu.strm_generate")
+ return (
+ ]()`
+ regex: /^generate strm \[(.+)]\((.*)\)$/,
+ title: (matches) => matches[1],
+ attrs: {
+ [t(`tasks.attr.strm_generate.path`)]: (matches) => matches[2] || "/",
+ },
+ }}
+ />
+ )
+}
+
+export default StrmGenerate