diff --git a/bun.lock b/bun.lock index 62a08ea..beba169 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "holistream", "dependencies": { + "@hattip/adapter-node": "^0.0.49", "@hono/node-server": "^1.19.11", "@pinia/colada": "^0.21.7", "@unhead/vue": "^2.1.10", @@ -13,7 +14,6 @@ "clsx": "^2.1.1", "hono": "^4.12.5", "i18next": "^25.8.14", - "i18next-browser-languagedetector": "^8.2.1", "i18next-http-backend": "^3.0.2", "i18next-vue": "^5.4.0", "is-mobile": "^5.0.0", @@ -170,6 +170,16 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@hattip/adapter-node": ["@hattip/adapter-node@0.0.49", "", { "dependencies": { "@hattip/core": "0.0.49", "@hattip/polyfills": "0.0.49", "@hattip/walk": "0.0.49" } }, "sha512-BE+Y8Q4U0YcH34FZUYU4DssGKOaZLbNL0zK57Z41UZp0m9kS79ZIolBmjjpPhTVpIlRY3Rs+uhXbVXKk7mUcJA=="], + + "@hattip/core": ["@hattip/core@0.0.49", "", {}, "sha512-3/ZJtC17cv8m6Sph8+nw4exUp9yhEf2Shi7HK6AHSUSBtaaQXZ9rJBVxTfZj3PGNOR/P49UBXOym/52WYKFTJQ=="], + + "@hattip/headers": ["@hattip/headers@0.0.49", "", { "dependencies": { "@hattip/core": "0.0.49" } }, "sha512-rrB2lEhTf0+MNVt5WdW184Ky706F1Ze9Aazn/R8c+/FMUYF9yjem2CgXp49csPt3dALsecrnAUOHFiV0LrrHXA=="], + + "@hattip/polyfills": ["@hattip/polyfills@0.0.49", "", { "dependencies": { "@hattip/core": "0.0.49", "@whatwg-node/fetch": "^0.9.22", "node-fetch-native": "^1.6.4" } }, "sha512-5g7W5s6Gq+HDxwULGFQ861yAnEx3yd9V8GDwS96HBZ1nM1u93vN+KTuwXvNsV7Z3FJmCrD/pgU8WakvchclYuA=="], + + "@hattip/walk": ["@hattip/walk@0.0.49", "", { "dependencies": { "@hattip/headers": "0.0.49", "cac": "^6.7.14", "mime-types": "^2.1.35" }, "bin": { "hattip-walk": "cli.js" } }, "sha512-AgJgKLooZyQnzMfoFg5Mo/aHM+HGBC9ExpXIjNqGimYTRgNbL/K7X5EM1kR2JY90BNKk9lo6Usq1T/nWFdT7TQ=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], @@ -236,6 +246,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@kamilkisiela/fast-url-parser": ["@kamilkisiela/fast-url-parser@1.1.4", "", {}, "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.115.0", "", { "os": "android", "cpu": "arm" }, "sha512-VoB2rhgoqgYf64d6Qs5emONQW8ASiTc0xp+aUE4JUhxjX+0pE3gblTYDO0upcN5vt9UlBNmUhAwfSifkfre7nw=="], @@ -420,6 +432,10 @@ "@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="], + "@whatwg-node/fetch": ["@whatwg-node/fetch@0.9.23", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.6.0", "urlpattern-polyfill": "^10.0.0" } }, "sha512-7xlqWel9JsmxahJnYVUj/LLxWcnA93DR4c9xlw3U814jWTiYalryiH1qToik1hOxweKKRLi4haXHM5ycRksPBA=="], + + "@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.6.0", "", { "dependencies": { "@kamilkisiela/fast-url-parser": "^1.1.4", "busboy": "^1.6.0", "fast-querystring": "^1.1.1", "tslib": "^2.6.3" } }, "sha512-tcZAhrpx6oVlkEsRngeTEEE7I5/QdLjeEz4IlekabGaESP7+Dkm/6a9KcF1KdCBB7mO9PXtBkwCuTCt8+UPg8Q=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], @@ -436,6 +452,8 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="], @@ -488,6 +506,10 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -502,8 +524,6 @@ "i18next": ["i18next@25.8.14", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA=="], - "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], - "i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="], "i18next-vue": ["i18next-vue@5.4.0", "", { "peerDependencies": { "i18next": ">=23", "vue": "^3.4.38" } }, "sha512-GDj0Xvmis5Xgcvo9gMBJMgJCtewYMLZP6gAEPDDGCMjA+QeB4uS4qUf1MK79mkz/FukhaJdC+nlj0y1qk6NO2Q=="], @@ -558,6 +578,10 @@ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "miniflare": ["miniflare@4.20260301.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260301.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], @@ -626,6 +650,8 @@ "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], @@ -674,6 +700,8 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], + "vite": ["vite@8.0.0-beta.16", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.6", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q=="], "vite-ssr-components": ["vite-ssr-components@0.5.2", "", { "dependencies": { "@babel/parser": "^7.27.2", "@babel/traverse": "^7.27.1", "picomatch": "^4.0.2" } }, "sha512-1a8YThRwyyu1gGjc1Ral9Q4uS+n0D4GydhbkVd9c1SA1YNgXyrOizttped87C1ItEznQzhiCyQjaOcYnXa0zMA=="], diff --git a/components.d.ts b/components.d.ts index 1f4dc24..6214150 100644 --- a/components.d.ts +++ b/components.d.ts @@ -24,6 +24,7 @@ declare module 'vue' { AppProgressBar: typeof import('./src/components/app/AppProgressBar.vue')['default'] AppSwitch: typeof import('./src/components/app/AppSwitch.vue')['default'] AppToastHost: typeof import('./src/components/app/AppToastHost.vue')['default'] + AppTopLoadingBar: typeof import('./src/components/AppTopLoadingBar.vue')['default'] ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] Bell: typeof import('./src/components/icons/Bell.vue')['default'] @@ -103,6 +104,7 @@ declare global { const AppProgressBar: typeof import('./src/components/app/AppProgressBar.vue')['default'] const AppSwitch: typeof import('./src/components/app/AppSwitch.vue')['default'] const AppToastHost: typeof import('./src/components/app/AppToastHost.vue')['default'] + const AppTopLoadingBar: typeof import('./src/components/AppTopLoadingBar.vue')['default'] const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default'] const Bell: typeof import('./src/components/icons/Bell.vue')['default'] diff --git a/docs.json b/docs.json index 08db3c7..419f386 100644 --- a/docs.json +++ b/docs.json @@ -18,6 +18,2224 @@ "host": "localhost:8080", "basePath": "/", "paths": { + "/ad-templates": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all VAST ad templates for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "ad-templates" + ], + "summary": "List Ad Templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/adtemplates.TemplateListPayload" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a VAST ad template for the current user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ad-templates" + ], + "summary": "Create Ad Template", + "parameters": [ + { + "description": "Ad template payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/adtemplates.SaveAdTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/adtemplates.TemplatePayload" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/ad-templates/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a VAST ad template for the current user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ad-templates" + ], + "summary": "Update Ad Template", + "parameters": [ + { + "type": "string", + "description": "Ad Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Ad template payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/adtemplates.SaveAdTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/adtemplates.TemplatePayload" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a VAST ad template for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "ad-templates" + ], + "summary": "Delete Ad Template", + "parameters": [ + { + "type": "string", + "description": "Ad Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/ad-templates": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get paginated list of all ad templates across users (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List All Ad Templates", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Filter by user ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Search by name", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create an ad template for any user (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create Ad Template", + "parameters": [ + { + "description": "Ad template payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.SaveAdminAdTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/ad-templates/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get ad template detail (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get Ad Template Detail", + "parameters": [ + { + "type": "string", + "description": "Ad Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an ad template for any user (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update Ad Template", + "parameters": [ + { + "type": "string", + "description": "Ad Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Ad template payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.SaveAdminAdTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete any ad template by ID (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete Ad Template (Admin)", + "parameters": [ + { + "type": "string", + "description": "Ad Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/dashboard": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get system-wide statistics for the admin dashboard", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Admin Dashboard", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.DashboardPayload" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/payments": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get paginated list of all payments across users (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List All Payments", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Filter by user ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a manual subscription charge for a user (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create Payment", + "parameters": [ + { + "description": "Payment payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.CreateAdminPaymentRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/payments/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get payment detail (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get Payment Detail", + "parameters": [ + { + "type": "string", + "description": "Payment ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update payment status safely without hard delete (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update Payment", + "parameters": [ + { + "type": "string", + "description": "Payment ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Payment update payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UpdateAdminPaymentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/plans": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all plans with usage counts (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List Plans", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a plan (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create Plan", + "parameters": [ + { + "description": "Plan payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.SavePlanRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/plans/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a plan (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update Plan", + "parameters": [ + { + "type": "string", + "description": "Plan ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Plan payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.SavePlanRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a plan, or deactivate it if already used (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete Plan", + "parameters": [ + { + "type": "string", + "description": "Plan ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get paginated list of all users (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List Users", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Search by email or username", + "name": "search", + "in": "query" + }, + { + "type": "string", + "description": "Filter by role", + "name": "role", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a user from admin panel (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create User", + "parameters": [ + { + "description": "User payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.CreateAdminUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get detailed info about a single user (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get User Detail", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a user from admin panel (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update User", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "User payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UpdateAdminUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a user and their data (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete User", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/users/{id}/role": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Change user role (admin only). Valid: USER, ADMIN, BLOCK", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update User Role", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UpdateUserRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/videos": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get paginated list of all videos across users (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List All Videos", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Search by title", + "name": "search", + "in": "query" + }, + { + "type": "string", + "description": "Filter by user ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a manual video record for a user (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create Video", + "parameters": [ + { + "description": "Video payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.SaveAdminVideoRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/videos/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get video detail by ID (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get Video Detail", + "parameters": [ + { + "type": "string", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update video metadata and status (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update Video", + "parameters": [ + { + "type": "string", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Video payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.SaveAdminVideoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete any video by ID (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete Video (Admin)", + "parameters": [ + { + "type": "string", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/change-password": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Change the authenticated user's local password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Change Password", + "parameters": [ + { + "description": "Password payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/forgot-password": { + "post": { + "description": "Request password reset link", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Forgot Password", + "parameters": [ + { + "description": "Forgot password payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.ForgotPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/google/callback": { + "get": { + "description": "Callback for Google Login", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Google Callback", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/auth.UserPayload" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/google/login": { + "get": { + "description": "Redirect to Google for Login", + "tags": [ + "auth" + ], + "summary": "Google Login", + "responses": {} + } + }, + "/auth/login": { + "post": { + "description": "Login with email and password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login", + "parameters": [ + { + "description": "Login payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/auth.UserPayload" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Logout user and clear cookies", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Register a new user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register", + "parameters": [ + { + "description": "Registration payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/auth/reset-password": { + "post": { + "description": "Reset password using token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Reset Password", + "parameters": [ + { + "description": "Reset password payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/domains": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all whitelisted domains for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "domains" + ], + "summary": "List Domains", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add a domain to the current user's whitelist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "domains" + ], + "summary": "Create Domain", + "parameters": [ + { + "description": "Domain payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domains.CreateDomainRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/domains/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove a domain from the current user's whitelist", + "produces": [ + "application/json" + ], + "tags": [ + "domains" + ], + "summary": "Delete Domain", + "parameters": [ + { + "type": "string", + "description": "Domain ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get the authenticated user's profile payload", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get Current User", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the authenticated user's profile information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Update Current User", + "parameters": [ + { + "description": "Profile payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.UpdateMeRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Permanently delete the authenticated user's account and related data", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Delete My Account", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/me/clear-data": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove videos and settings-related resources for the authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Clear My Data", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/notifications": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get notifications for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "List Notifications", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete all notifications for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Clear Notifications", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/notifications/read-all": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark all notifications as read for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Mark All Notifications Read", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/notifications/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a single notification for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Delete Notification", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/notifications/{id}/read": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a single notification as read for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Mark Notification Read", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/payments": { "post": { "security": [ @@ -25,7 +2243,7 @@ "BearerAuth": [] } ], - "description": "Create a new payment", + "description": "Create a new payment for buying or renewing a plan", "consumes": [ "application/json" ], @@ -66,6 +2284,101 @@ "$ref": "#/definitions/response.Response" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/payments/history": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get payment history for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "payment" + ], + "summary": "List Payment History", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/payments/{id}/invoice": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Download invoice text for a payment or wallet top-up", + "produces": [ + "text/plain" + ], + "tags": [ + "payment" + ], + "summary": "Download Invoice", + "parameters": [ + { + "type": "string", + "description": "Payment ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -121,6 +2434,147 @@ } } }, + "/settings/preferences": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get notification, player, and locale preferences for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Get Preferences", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update notification, player, and locale preferences for the current user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Update Preferences", + "parameters": [ + { + "description": "Preferences payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/preferences.SettingsPreferencesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/usage": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get the authenticated user's total video count and total storage usage", + "produces": [ + "application/json" + ], + "tags": [ + "usage" + ], + "summary": "Get Usage", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/usage.UsagePayload" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/videos": { "get": { "security": [ @@ -330,10 +2784,673 @@ } } } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update title and description for a video owned by the current user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "video" + ], + "summary": "Update Video", + "parameters": [ + { + "type": "string", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Video payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/video.UpdateVideoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a video owned by the current user", + "produces": [ + "application/json" + ], + "tags": [ + "video" + ], + "summary": "Delete Video", + "parameters": [ + { + "type": "string", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/wallet/topups": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add funds to wallet balance for the current user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "payment" + ], + "summary": "Top Up Wallet", + "parameters": [ + { + "description": "Topup Info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/payment.TopupWalletRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } } } }, "definitions": { + "admin.CreateAdminPaymentRequest": { + "type": "object", + "required": [ + "payment_method", + "plan_id", + "term_months", + "user_id" + ], + "properties": { + "payment_method": { + "type": "string" + }, + "plan_id": { + "type": "string" + }, + "term_months": { + "type": "integer" + }, + "topup_amount": { + "type": "number" + }, + "user_id": { + "type": "string" + } + } + }, + "admin.CreateAdminUserRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string", + "minLength": 6 + }, + "plan_id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.DashboardPayload": { + "type": "object", + "properties": { + "active_subscriptions": { + "type": "integer" + }, + "new_users_today": { + "type": "integer" + }, + "new_videos_today": { + "type": "integer" + }, + "total_ad_templates": { + "type": "integer" + }, + "total_payments": { + "type": "integer" + }, + "total_revenue": { + "type": "number" + }, + "total_storage_used": { + "type": "integer" + }, + "total_users": { + "type": "integer" + }, + "total_videos": { + "type": "integer" + } + } + }, + "admin.SaveAdminAdTemplateRequest": { + "type": "object", + "required": [ + "name", + "user_id", + "vast_tag_url" + ], + "properties": { + "ad_format": { + "type": "string" + }, + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "vast_tag_url": { + "type": "string" + } + } + }, + "admin.SaveAdminVideoRequest": { + "type": "object", + "required": [ + "size", + "title", + "url", + "user_id" + ], + "properties": { + "ad_template_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "format": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "admin.SavePlanRequest": { + "type": "object", + "required": [ + "cycle", + "name", + "price", + "storage_limit", + "upload_limit" + ], + "properties": { + "cycle": { + "type": "string" + }, + "description": { + "type": "string" + }, + "features": { + "type": "array", + "items": { + "type": "string" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + }, + "storage_limit": { + "type": "integer" + }, + "upload_limit": { + "type": "integer" + } + } + }, + "admin.UpdateAdminPaymentRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string" + } + } + }, + "admin.UpdateAdminUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "plan_id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.UpdateUserRoleRequest": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "type": "string" + } + } + }, + "adtemplates.SaveAdTemplateRequest": { + "type": "object", + "required": [ + "name", + "vast_tag_url" + ], + "properties": { + "ad_format": { + "type": "string" + }, + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "vast_tag_url": { + "type": "string" + } + } + }, + "adtemplates.TemplateListPayload": { + "type": "object", + "properties": { + "templates": { + "type": "array", + "items": { + "$ref": "#/definitions/manual.AdTemplate" + } + } + } + }, + "adtemplates.TemplatePayload": { + "type": "object", + "properties": { + "template": { + "$ref": "#/definitions/manual.AdTemplate" + } + } + }, + "auth.ChangePasswordRequest": { + "type": "object", + "required": [ + "current_password", + "new_password" + ], + "properties": { + "current_password": { + "type": "string" + }, + "new_password": { + "type": "string", + "minLength": 6 + } + } + }, + "auth.ForgotPasswordRequest": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "auth.LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "auth.RegisterRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string", + "minLength": 6 + }, + "username": { + "type": "string" + } + } + }, + "auth.ResetPasswordRequest": { + "type": "object", + "required": [ + "new_password", + "token" + ], + "properties": { + "new_password": { + "type": "string", + "minLength": 6 + }, + "token": { + "type": "string" + } + } + }, + "auth.UpdateMeRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "language": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "auth.UserPayload": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "google_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "language": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "plan_expires_at": { + "type": "string" + }, + "plan_expiring_soon": { + "type": "boolean" + }, + "plan_id": { + "type": "string" + }, + "plan_payment_method": { + "type": "string" + }, + "plan_started_at": { + "type": "string" + }, + "plan_term_months": { + "type": "integer" + }, + "role": { + "type": "string" + }, + "storage_used": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + }, + "wallet_balance": { + "type": "number" + } + } + }, + "domains.CreateDomainRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "manual.AdTemplate": { + "type": "object", + "properties": { + "ad_format": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "vast_tag_url": { + "type": "string" + } + } + }, "model.Plan": { "type": "object", "properties": { @@ -347,7 +3464,10 @@ "type": "integer" }, "features": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "id": { "type": "string" @@ -434,15 +3554,77 @@ "payment.CreatePaymentRequest": { "type": "object", "required": [ - "amount", - "plan_id" + "payment_method", + "plan_id", + "term_months" + ], + "properties": { + "payment_method": { + "type": "string" + }, + "plan_id": { + "type": "string" + }, + "term_months": { + "type": "integer" + }, + "topup_amount": { + "type": "number" + } + } + }, + "payment.TopupWalletRequest": { + "type": "object", + "required": [ + "amount" ], "properties": { "amount": { "type": "number" + } + } + }, + "preferences.SettingsPreferencesRequest": { + "type": "object", + "properties": { + "airplay": { + "type": "boolean" }, - "plan_id": { + "autoplay": { + "type": "boolean" + }, + "chromecast": { + "type": "boolean" + }, + "email_notifications": { + "type": "boolean" + }, + "language": { "type": "string" + }, + "locale": { + "type": "string" + }, + "loop": { + "type": "boolean" + }, + "marketing_notifications": { + "type": "boolean" + }, + "muted": { + "type": "boolean" + }, + "pip": { + "type": "boolean" + }, + "push_notifications": { + "type": "boolean" + }, + "show_controls": { + "type": "boolean" + }, + "telegram_notifications": { + "type": "boolean" } } }, @@ -458,6 +3640,20 @@ } } }, + "usage.UsagePayload": { + "type": "object", + "properties": { + "total_storage": { + "type": "integer" + }, + "total_videos": { + "type": "integer" + }, + "user_id": { + "type": "string" + } + } + }, "video.CreateVideoRequest": { "type": "object", "required": [ @@ -488,6 +3684,23 @@ } } }, + "video.UpdateVideoRequest": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "ad_template_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "video.UploadURLRequest": { "type": "object", "required": [ diff --git a/package.json b/package.json index fed71dc..360251a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "tail": "wrangler tail" }, "dependencies": { + "@hattip/adapter-node": "^0.0.49", "@hono/node-server": "^1.19.11", "@pinia/colada": "^0.21.7", "@unhead/vue": "^2.1.10", @@ -18,7 +19,6 @@ "clsx": "^2.1.1", "hono": "^4.12.5", "i18next": "^25.8.14", - "i18next-browser-languagedetector": "^8.2.1", "i18next-http-backend": "^3.0.2", "i18next-vue": "^5.4.0", "is-mobile": "^5.0.0", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 031f2c9..d200e56 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -10,7 +10,7 @@ "actions": "Actions", "status": "Status", "videos": "Videos", - "selected": "{count} selected", + "selected": "{{count}} selected", "copy": "Copy" }, "app": { @@ -211,6 +211,10 @@ "chromecast": { "title": "Chromecast", "description": "Allow casting to Chromecast devices" + }, + "encrytion_m3u8": { + "title": "HLS Encryption (m3u8)", + "description": "Enable encryption for HLS streams (Anti-download, VIP only)" } } }, @@ -240,10 +244,12 @@ "clearDataReject": "Cancel" }, "toast": { - "deleteAccountSummary": "Account deletion requested", - "deleteAccountDetail": "Your account deletion request has been submitted.", + "deleteAccountSummary": "Account deleted", + "deleteAccountDetail": "Your account and associated data have been permanently deleted.", "clearDataSummary": "Data cleared", - "clearDataDetail": "All your data has been permanently deleted." + "clearDataDetail": "All your data has been permanently deleted.", + "failedSummary": "Action failed", + "failedDetail": "Failed to complete the requested action." } }, "domainsDns": { @@ -282,13 +288,18 @@ "removedSummary": "Domain Removed", "removedDetail": "{domain} has been removed from your whitelist.", "copiedSummary": "Copied", - "copiedDetail": "Embed code copied to clipboard." + "copiedDetail": "Embed code copied to clipboard.", + "failedSummary": "Action failed", + "failedDetail": "Failed to load or update domains." } }, "adsVast": { "createTemplate": "Create Template", "infoBanner": "VAST (Video Ad Serving Template) is an XML schema for serving ad tags to video players.", - "createdOn": "Created {date}", + "readOnlyTitle": "Upgrade required", + "readOnlyMessage": "Ads & VAST is read-only on the free plan. Upgrade your plan to create, edit, delete, enable, or set default templates.", + "defaultBadge": "Default", + "createdOn": "Created {{date}}", "emptyTitle": "No VAST templates yet", "emptySubtitle": "Create a template to start monetizing your videos", "formats": { @@ -300,6 +311,10 @@ "enabled": "enabled", "disabled": "disabled" }, + "actions": { + "default": "Default", + "setDefault": "Set default" + }, "table": { "template": "Template", "format": "Format", @@ -315,6 +330,10 @@ "adFormat": "Ad Format", "adInterval": "Ad Interval (seconds)", "adIntervalPlaceholder": "30", + "defaultLabel": "Default template", + "defaultCheckbox": "Use this template by default for new videos", + "defaultHint": "When enabled, newly created videos automatically use this active template.", + "defaultDisabledHint": "Enable this template before setting it as default.", "update": "Update", "create": "Create" }, @@ -339,11 +358,17 @@ "createdDetail": "VAST template has been created.", "enabledSummary": "Template Enabled", "disabledSummary": "Template Disabled", + "defaultUpdatedSummary": "Default Updated", + "defaultUpdatedDetail": "{name} is now the default template for new videos.", + "upgradeRequiredSummary": "Upgrade required", + "upgradeRequiredDetail": "Upgrade your plan to manage Ads & VAST.", "toggleDetail": "{name} has been {state}.", "deletedSummary": "Template Deleted", "deletedDetail": "VAST template has been removed.", "copiedSummary": "Copied", - "copiedDetail": "URL copied to clipboard." + "copiedDetail": "URL copied to clipboard.", + "failedSummary": "Action failed", + "failedDetail": "Failed to load or update VAST templates." } }, "profile": { @@ -353,7 +378,7 @@ "username": "Username", "email": "Email Address", "storageUsage": "Storage Usage", - "storageUsedOfLimit": "{used} of {limit} used", + "storageUsedOfLimit": "{{used}} of {{limit}} used", "editProfile": "Edit Profile", "changePassword": "Change Password" }, @@ -373,26 +398,51 @@ }, "billing": { "walletBalance": "Wallet Balance", - "currentBalance": "Current balance: {balance}", + "currentBalance": "Current balance: {{balance}}", "topUp": "Top Up", "availablePlans": "Available Plans", "availablePlansHint": "Choose the plan that best fits your needs", - "planStorage": "{storage} Storage", - "planDuration": "{duration} Max Duration", - "planUploads": "{count} Uploads / day", + "planStorage": "{{storage}} Storage", + "planDuration": "{{duration}} Max Duration", + "planUploads": "{{count}} Uploads / day", "currentPlan": "Current Plan", "processing": "Processing...", "upgrade": "Upgrade", "storage": "Storage", - "storageUsedOfLimit": "{used} of {limit} used", - "monthlyUploads": "Monthly Uploads", - "uploadsUsedOfLimit": "{used} of {limit} uploads", + "storageUsedOfLimit": "{{used}} of {{limit}} used", + "totalVideos": "Total videos", + "totalVideosUsedOfLimit": "{{used}} of {{limit}} videos", "paymentHistory": "Payment History", "paymentHistorySubtitle": "Your past payments and invoices", "noPaymentHistory": "No payment history found.", "download": "Download", - "durationMinutes": "{minutes} mins", + "durationMinutes": "{{minutes}} mins", "unknownPlan": "Unknown", + "walletTopup": "Wallet Top-up", + "termOption": "{{months}} month", + "termOption_other": "{{months}} months", + "paymentMethod": { + "wallet": "Wallet balance", + "topup": "Top up and pay" + }, + "subscription": { + "activeTitle": "Plan active", + "activeDescription": " {{plan}} is active until {{date}}", + "expiringTitle": "Expiring soon", + "expiringDescription": " {{plan}} expires on {{date}}", + "expiredTitle": "Plan expired", + "expiredDescription": "Your last subscription ended on {{date}}", + "freeTitle": "Free access", + "freeDescription": "You are currently using the free plan." + }, + "history": { + "validUntil": "Valid until {{date}}" + }, + "cycle": { + "MONTHLY": "Monthly", + "QUARTERLY": "Quarterly", + "YEARLY": "Yearly" + }, "table": { "date": "Date", "amount": "Amount", @@ -405,6 +455,31 @@ "failed": "Failed", "pending": "Pending" }, + "upgradeDialog": { + "title": "Upgrade or renew plan", + "selectedPlan": "Selected plan", + "basePrice": "Base monthly price", + "perMonthBase": "Used as the monthly base for the selected term", + "termTitle": "Billing term", + "termHint": "Choose how long you want to activate or extend this plan.", + "totalLabel": "Total", + "walletBalanceLabel": "Wallet balance", + "shortfallLabel": "Shortfall", + "paymentMethodTitle": "How do you want to pay?", + "paymentMethodHint": "If your wallet is short, you can top up the difference and complete the plan purchase in one flow.", + "walletOptionDescription": "Try using your current wallet balance first.", + "topupOptionDescription": "Top up at least {{shortfall}} to complete the purchase.", + "walletCoveredHint": "Your wallet balance already covers this purchase.", + "walletInsufficientHint": "Your wallet is short by {{shortfall}}. Switch to the top-up option to complete the purchase.", + "topupAmountLabel": "Top-up amount", + "topupAmountPlaceholder": "Enter top-up amount", + "topupAmountHint": "Any amount above {{shortfall}} will stay in your wallet after the upgrade.", + "payWithWallet": "Pay with wallet", + "topupAndUpgrade": "Top up and upgrade", + "choosePlan": "Choose plan", + "selecting": "Opening...", + "footerHint": "The new term will be added from your current expiry if your subscription is still active." + }, "topupDialog": { "title": "Top Up Wallet", "subtitle": "Select an amount or enter a custom amount to add to your wallet.", @@ -415,17 +490,19 @@ }, "toast": { "subscriptionSuccessSummary": "Subscription Successful", - "subscriptionSuccessDetail": "Successfully subscribed to {plan}", + "subscriptionSuccessDetail": "Successfully activated {{plan}} for {{term}}", "subscriptionFailedSummary": "Subscription Failed", "subscriptionFailedDetail": "Failed to subscribe", "topupSuccessSummary": "Top-up Successful", - "topupSuccessDetail": "{amount} has been added to your wallet.", + "topupSuccessDetail": "{{amount}} has been added to your wallet.", "topupFailedSummary": "Top-up Failed", "topupFailedDetail": "Failed to process top-up.", "downloadingSummary": "Downloading", "downloadingDetail": "Downloading invoice #{invoiceId}...", "downloadedSummary": "Downloaded", - "downloadedDetail": "Invoice #{invoiceId} downloaded successfully" + "downloadedDetail": "Invoice #{invoiceId} downloaded successfully", + "downloadFailedSummary": "Download Failed", + "downloadFailedDetail": "Failed to download invoice." } }, "securityConnected": { @@ -561,7 +638,6 @@ "totalVideos": "Total Videos", "totalViews": "Total Views", "storageUsed": "Storage Used", - "uploadsThisMonth": "Uploads This Month", "trendVsLastMonth": "vs last month" }, "quickActions": { @@ -608,7 +684,7 @@ }, "storage": { "title": "Storage Usage", - "usedOfLimit": "{used} of {limit} used", + "usedOfLimit": "{{used}} of {{limit}} used", "breakdown": { "videos": "Videos", "thumbnails": "Thumbnails & Assets", @@ -628,19 +704,19 @@ "uploadAction": "Upload Video", "uploadDropTitle": "Drop to upload", "uploadDropSubtitle": "Files will be added to the upload queue", - "deleteSelectedConfirm": "Delete {count} videos?", + "deleteSelectedConfirm": "Delete {{count}} videos?", "deleteSingleConfirm": "Are you sure you want to delete this video?", "retry": "Try Again", "emptyTitle": "No videos found", "emptyDescription": "You haven't uploaded any videos yet. Start by uploading your first video!", "emptyAction": "Upload Video", "duplicateSummary": "Duplicate files skipped", - "duplicateDetailOne": "{count} file is already in the queue.", - "duplicateDetailOther": "{count} files are already in the queue." + "duplicateDetailOne": "{{count}} file is already in the queue.", + "duplicateDetailOther": "{{count}} files are already in the queue." }, "filters": { "searchPlaceholder": "Search videos...", - "rangeOfTotal": "{first}–{last} of {total}", + "rangeOfTotal": "{first}–{last} of {{total}}", "previousPageAria": "Previous page", "nextPageAria": "Next page", "allStatus": "All Status", @@ -660,7 +736,7 @@ "delete": "Delete" }, "bulk": { - "selected": "{count} selected", + "selected": "{{count}} selected", "delete": "Delete" }, "copyModal": { @@ -684,10 +760,13 @@ "title": "Edit video", "titleLabel": "Title", "titlePlaceholder": "Enter video title", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Enter video description", + "adTemplateLabel": "Ad Template", + "adTemplateNone": "No ads", + "adTemplateDefault": "Default", + "adTemplateUpgradeHint": "Upgrade your plan to customize ad templates for this video.", + "adTemplateNoAdsHint": "No ad template selected. This video will play without ads.", "subtitlesTitle": "Subtitles", - "subtitleTracks": "{count} tracks", + "subtitleTracks": "{{count}} tracks", "noSubtitles": "No subtitles uploaded yet", "uploadSubtitle": "Upload Subtitle", "subtitleFile": "Subtitle File (VTT, SRT, ASS, SSA)", @@ -774,8 +853,8 @@ "payments": "Payments" }, "stats": { - "total": "{count} notifications", - "unread": "{count} unread" + "total": "{{count}} notifications", + "unread": "{{count}} unread" }, "actions": { "markAllRead": "Mark all read", @@ -796,9 +875,9 @@ "subtitle": "You're all caught up! Check back later." }, "time": { - "minutesAgo": "{count} minutes ago", - "hoursAgo": "{count} hours ago", - "daysAgo": "{count} days ago" + "minutesAgo": "{{count}} minutes ago", + "hoursAgo": "{{count}} hours ago", + "daysAgo": "{{count}} days ago" }, "mocks": { "videoProcessed": { @@ -830,23 +909,23 @@ "upload": { "dialog": { "title": "Upload Videos", - "subtitle": "Add up to {maxItems} videos per batch", + "subtitle": "Add up to {{maxItems}} videos per batch", "mode": { "local": "Local", "remote": "Remote URL" }, "queueFullTitle": "Queue is full", - "queueFullDescription": "Maximum {maxItems} videos per batch. Start or clear the current queue first.", - "slotsRemaining": "{remaining} / {maxItems} slots remaining", + "queueFullDescription": "Maximum {{maxItems}} videos per batch. Start or clear the current queue first.", + "slotsRemaining": "{{remaining}} / {{maxItems}} slots remaining", "formatsHint": "MP4, MOV, MKV · max 10 GB per file", "close": "Close", - "startUpload": "Start Upload ({count})", + "startUpload": "Start Upload ({{count}})", "duplicateFilesSummary": "Duplicate files skipped", - "duplicateFilesDetailOne": "{count} file is already in the queue.", - "duplicateFilesDetailOther": "{count} files are already in the queue.", + "duplicateFilesDetailOne": "{{count}} file is already in the queue.", + "duplicateFilesDetailOther": "{{count}} files are already in the queue.", "duplicateUrlsSummary": "Duplicate URLs skipped", - "duplicateUrlsDetailOne": "{count} URL is already in the queue.", - "duplicateUrlsDetailOther": "{count} URLs are already in the queue." + "duplicateUrlsDetailOne": "{{count}} URL is already in the queue.", + "duplicateUrlsDetailOther": "{{count}} URLs are already in the queue." }, "dropzone": { "releaseToAdd": "Release to add", @@ -869,7 +948,7 @@ "status": { "pending": "Pending", "uploading": "Uploading...", - "uploadingThreads": "Uploading · {threads} threads", + "uploadingThreads": "Uploading · {{threads}} threads", "processing": "Processing...", "complete": "Done", "error": "Failed", @@ -882,7 +961,7 @@ }, "bulkActions": { "title": "Quick Settings", - "applyToPending": "Apply to {count} pending files", + "applyToPending": "Apply to {{count}} pending files", "selectCategory": "Select category...", "category": { "learning": "Learning", @@ -895,15 +974,15 @@ }, "indicator": { "allDone": "All done", - "uploading": "Uploading {count} files...", - "waiting": "{count} files waiting", - "completeProgress": "{complete} of {total} complete", + "uploading": "Uploading {{count}} files...", + "waiting": "{{count}} files waiting", + "completeProgress": "{{complete}} of {{total}} complete", "start": "Start", "viewVideos": "View Videos", "addMoreFiles": "Add more files" }, "errors": { - "chunkUploadFailed": "Failed to upload chunk {index}", + "chunkUploadFailed": "Failed to upload chunk {{index}}", "mergeFailed": "Merge failed" } }, diff --git a/public/locales/vi/translation.json b/public/locales/vi/translation.json index f001cea..93190d0 100644 --- a/public/locales/vi/translation.json +++ b/public/locales/vi/translation.json @@ -10,7 +10,7 @@ "actions": "Hành động", "status": "Trạng thái", "videos": "Video", - "selected": "{count} mục đã chọn", + "selected": "{{count}} mục đã chọn", "copy": "Sao chép" }, "app": { @@ -211,6 +211,10 @@ "chromecast": { "title": "Chromecast", "description": "Cho phép cast tới thiết bị Chromecast" + }, + "encrytion_m3u8": { + "title": "Mã hóa HLS (m3u8)", + "description": "Bật mã hóa cho luồng HLS (Chống download trái phép, chỉ dành cho VIP)" } } }, @@ -240,10 +244,12 @@ "clearDataReject": "Hủy" }, "toast": { - "deleteAccountSummary": "Đã gửi yêu cầu xóa tài khoản", - "deleteAccountDetail": "Yêu cầu xóa tài khoản của bạn đã được gửi.", + "deleteAccountSummary": "Đã xóa tài khoản", + "deleteAccountDetail": "Tài khoản và dữ liệu liên quan của bạn đã bị xóa vĩnh viễn.", "clearDataSummary": "Đã xóa dữ liệu", - "clearDataDetail": "Toàn bộ dữ liệu của bạn đã bị xóa vĩnh viễn." + "clearDataDetail": "Toàn bộ dữ liệu của bạn đã bị xóa vĩnh viễn.", + "failedSummary": "Thao tác thất bại", + "failedDetail": "Không thể hoàn tất thao tác đã yêu cầu." } }, "domainsDns": { @@ -282,13 +288,18 @@ "removedSummary": "Đã xóa tên miền", "removedDetail": "{domain} đã được xóa khỏi whitelist.", "copiedSummary": "Đã sao chép", - "copiedDetail": "Đã sao chép mã nhúng vào clipboard." + "copiedDetail": "Đã sao chép mã nhúng vào clipboard.", + "failedSummary": "Thao tác thất bại", + "failedDetail": "Không thể tải hoặc cập nhật danh sách tên miền." } }, "adsVast": { "createTemplate": "Tạo mẫu", "infoBanner": "VAST (Video Ad Serving Template) là schema XML dùng để phân phối ad tags cho trình phát video.", - "createdOn": "Tạo ngày {date}", + "readOnlyTitle": "Cần nâng cấp gói", + "readOnlyMessage": "Ads & VAST ở chế độ chỉ đọc với gói free. Hãy nâng cấp gói để tạo, sửa, xóa, bật/tắt hoặc đặt mẫu mặc định.", + "defaultBadge": "Mặc định", + "createdOn": "Tạo ngày {{date}}", "emptyTitle": "Chưa có mẫu VAST", "emptySubtitle": "Tạo mẫu để bắt đầu kiếm tiền từ video", "formats": { @@ -300,6 +311,10 @@ "enabled": "bật", "disabled": "tắt" }, + "actions": { + "default": "Mặc định", + "setDefault": "Đặt mặc định" + }, "table": { "template": "Mẫu", "format": "Định dạng", @@ -315,6 +330,10 @@ "adFormat": "Định dạng quảng cáo", "adInterval": "Khoảng cách quảng cáo (giây)", "adIntervalPlaceholder": "30", + "defaultLabel": "Mẫu mặc định", + "defaultCheckbox": "Dùng mẫu này mặc định cho video mới", + "defaultHint": "Khi bật, video mới tạo sẽ tự động dùng mẫu đang active này.", + "defaultDisabledHint": "Hãy bật mẫu này trước khi đặt làm mặc định.", "update": "Cập nhật", "create": "Tạo" }, @@ -339,11 +358,17 @@ "createdDetail": "Mẫu VAST đã được tạo.", "enabledSummary": "Đã bật mẫu", "disabledSummary": "Đã tắt mẫu", + "defaultUpdatedSummary": "Đã cập nhật mặc định", + "defaultUpdatedDetail": "{name} hiện là mẫu mặc định cho video mới.", + "upgradeRequiredSummary": "Cần nâng cấp gói", + "upgradeRequiredDetail": "Hãy nâng cấp gói để quản lý Ads & VAST.", "toggleDetail": "{name} đã được {state}.", "deletedSummary": "Đã xóa mẫu", "deletedDetail": "Mẫu VAST đã được gỡ bỏ.", "copiedSummary": "Đã sao chép", - "copiedDetail": "Đã sao chép URL vào clipboard." + "copiedDetail": "Đã sao chép URL vào clipboard.", + "failedSummary": "Thao tác thất bại", + "failedDetail": "Không thể tải hoặc cập nhật mẫu VAST." } }, "profile": { @@ -353,7 +378,7 @@ "username": "Tên người dùng", "email": "Địa chỉ email", "storageUsage": "Dung lượng sử dụng", - "storageUsedOfLimit": "Đã dùng {used} trên {limit}", + "storageUsedOfLimit": "Đã dùng {{used}} trên {{limit}}", "editProfile": "Chỉnh sửa hồ sơ", "changePassword": "Đổi mật khẩu" }, @@ -373,26 +398,50 @@ }, "billing": { "walletBalance": "Số dư ví", - "currentBalance": "Số dư hiện tại: {balance}", + "currentBalance": "Số dư hiện tại: {{balance}}", "topUp": "Nạp tiền", "availablePlans": "Các gói khả dụng", "availablePlansHint": "Chọn gói phù hợp nhất với nhu cầu của bạn", - "planStorage": "{storage} dung lượng", - "planDuration": "{duration} thời lượng tối đa", - "planUploads": "{count} lượt tải / ngày", + "planStorage": "{{storage}} dung lượng", + "planDuration": "{{duration}} thời lượng tối đa", + "planUploads": "{{count}} lượt tải / ngày", "currentPlan": "Gói hiện tại", "processing": "Đang xử lý...", "upgrade": "Nâng cấp", "storage": "Dung lượng", - "storageUsedOfLimit": "Đã dùng {used} trên {limit}", - "monthlyUploads": "Lượt tải tháng này", - "uploadsUsedOfLimit": "{used} trên {limit} lượt tải", + "storageUsedOfLimit": "Đã dùng {{used}} trên {{limit}}", + "totalVideos": "Tổng video", + "totalVideosUsedOfLimit": "{{used}} trên {{limit}} video", "paymentHistory": "Lịch sử thanh toán", "paymentHistorySubtitle": "Các khoản thanh toán và hóa đơn trước đây của bạn", "noPaymentHistory": "Không tìm thấy lịch sử thanh toán.", "download": "Tải xuống", - "durationMinutes": "{minutes} phút", + "durationMinutes": "{{minutes}} phút", "unknownPlan": "Không xác định", + "walletTopup": "Nạp tiền ví", + "termOption": "{{months}} tháng", + "paymentMethod": { + "wallet": "Dùng số dư ví", + "topup": "Nạp thêm và thanh toán" + }, + "subscription": { + "activeTitle": "Gói đang hoạt động", + "activeDescription": " {{plan}} có hiệu lực đến {{date}}", + "expiringTitle": "Sắp hết hạn", + "expiringDescription": " {{plan}} sẽ hết hạn vào {{date}}", + "expiredTitle": "Gói đã hết hạn", + "expiredDescription": "Gói gần nhất của bạn đã kết thúc vào {{date}}", + "freeTitle": "Gói miễn phí", + "freeDescription": "Bạn hiện đang sử dụng gói miễn phí." + }, + "history": { + "validUntil": "Hiệu lực đến {{date}}" + }, + "cycle": { + "MONTHLY": "Tháng", + "QUARTERLY": "Quý", + "YEARLY": "Năm" + }, "table": { "date": "Ngày", "amount": "Số tiền", @@ -405,6 +454,31 @@ "failed": "Thất bại", "pending": "Đang chờ" }, + "upgradeDialog": { + "title": "Nâng cấp hoặc gia hạn gói", + "selectedPlan": "Gói đã chọn", + "basePrice": "Giá cơ bản mỗi tháng", + "perMonthBase": "Được dùng làm giá cơ sở cho kỳ hạn bạn chọn", + "termTitle": "Kỳ hạn thanh toán", + "termHint": "Chọn thời gian bạn muốn kích hoạt hoặc gia hạn gói này.", + "totalLabel": "Tổng tiền", + "walletBalanceLabel": "Số dư ví", + "shortfallLabel": "Còn thiếu", + "paymentMethodTitle": "Bạn muốn thanh toán theo cách nào?", + "paymentMethodHint": "Nếu ví chưa đủ, bạn có thể nạp phần thiếu và hoàn tất mua gói trong cùng một flow.", + "walletOptionDescription": "Thử dùng số dư ví hiện tại trước.", + "topupOptionDescription": "Nạp ít nhất {{shortfall}} để hoàn tất giao dịch.", + "walletCoveredHint": "Số dư ví hiện tại đã đủ để thanh toán gói này.", + "walletInsufficientHint": "Ví của bạn còn thiếu {{shortfall}}. Hãy chuyển sang phương án nạp thêm để hoàn tất giao dịch.", + "topupAmountLabel": "Số tiền nạp thêm", + "topupAmountPlaceholder": "Nhập số tiền muốn nạp", + "topupAmountHint": "Phần tiền nạp vượt quá {{shortfall}} sẽ được giữ lại trong ví sau khi nâng cấp.", + "payWithWallet": "Thanh toán bằng ví", + "topupAndUpgrade": "Nạp thêm và nâng cấp", + "choosePlan": "Chọn gói này", + "selecting": "Đang mở...", + "footerHint": "Nếu gói hiện tại vẫn còn hạn, kỳ hạn mới sẽ được cộng tiếp từ ngày hết hạn hiện tại." + }, "topupDialog": { "title": "Nạp tiền vào ví", "subtitle": "Chọn số tiền hoặc nhập số tiền tùy chỉnh để nạp vào ví.", @@ -415,17 +489,19 @@ }, "toast": { "subscriptionSuccessSummary": "Đăng ký thành công", - "subscriptionSuccessDetail": "Đăng ký gói {plan} thành công", + "subscriptionSuccessDetail": "Đã kích hoạt {{plan}} trong {{term}}", "subscriptionFailedSummary": "Đăng ký thất bại", "subscriptionFailedDetail": "Không thể đăng ký gói", "topupSuccessSummary": "Nạp tiền thành công", - "topupSuccessDetail": "{amount} đã được cộng vào ví của bạn.", + "topupSuccessDetail": "{{amount}} đã được cộng vào ví của bạn.", "topupFailedSummary": "Nạp tiền thất bại", "topupFailedDetail": "Không thể xử lý nạp tiền.", "downloadingSummary": "Đang tải", "downloadingDetail": "Đang tải hóa đơn #{invoiceId}...", "downloadedSummary": "Đã tải xong", - "downloadedDetail": "Hóa đơn #{invoiceId} đã được tải thành công" + "downloadedDetail": "Hóa đơn #{invoiceId} đã được tải thành công", + "downloadFailedSummary": "Tải xuống thất bại", + "downloadFailedDetail": "Không thể tải hóa đơn." } }, "securityConnected": { @@ -561,7 +637,6 @@ "totalVideos": "Tổng số video", "totalViews": "Tổng lượt xem", "storageUsed": "Dung lượng đã dùng", - "uploadsThisMonth": "Lượt tải lên tháng này", "trendVsLastMonth": "so với tháng trước" }, "quickActions": { @@ -608,7 +683,7 @@ }, "storage": { "title": "Sử dụng dung lượng", - "usedOfLimit": "Đã dùng {used} trên {limit}", + "usedOfLimit": "Đã dùng {{used}} trên {{limit}}", "breakdown": { "videos": "Video", "thumbnails": "Thumbnail & tài nguyên", @@ -628,15 +703,15 @@ "uploadAction": "Tải video lên", "uploadDropTitle": "Thả để tải lên", "uploadDropSubtitle": "Tệp sẽ được thêm vào hàng đợi tải lên", - "deleteSelectedConfirm": "Xóa {count} video?", + "deleteSelectedConfirm": "Xóa {{count}} video?", "deleteSingleConfirm": "Bạn có chắc muốn xóa video này?", "retry": "Thử lại", "emptyTitle": "Không có video", "emptyDescription": "Bạn chưa tải video nào. Hãy bắt đầu với video đầu tiên!", "emptyAction": "Tải video lên", "duplicateSummary": "Đã bỏ qua tệp trùng lặp", - "duplicateDetailOne": "{count} tệp đã có trong hàng đợi.", - "duplicateDetailOther": "{count} tệp đã có trong hàng đợi." + "duplicateDetailOne": "{{count}} tệp đã có trong hàng đợi.", + "duplicateDetailOther": "{{count}} tệp đã có trong hàng đợi." }, "filters": { "searchPlaceholder": "Tìm kiếm video...", @@ -660,7 +735,7 @@ "delete": "Xóa" }, "bulk": { - "selected": "{count} mục đã chọn", + "selected": "{{count}} mục đã chọn", "delete": "Xóa" }, "copyModal": { @@ -684,10 +759,13 @@ "title": "Chỉnh sửa video", "titleLabel": "Tiêu đề", "titlePlaceholder": "Nhập tiêu đề video", - "descriptionLabel": "Mô tả", - "descriptionPlaceholder": "Nhập mô tả video", + "adTemplateLabel": "Mẫu quảng cáo", + "adTemplateNone": "Không có quảng cáo", + "adTemplateDefault": "Mặc định", + "adTemplateUpgradeHint": "Nâng cấp gói để tùy chỉnh mẫu quảng cáo cho video này.", + "adTemplateNoAdsHint": "Chưa chọn mẫu quảng cáo. Video này sẽ phát không có quảng cáo.", "subtitlesTitle": "Phụ đề", - "subtitleTracks": "{count} track", + "subtitleTracks": "{{count}} track", "noSubtitles": "Chưa có phụ đề", "uploadSubtitle": "Tải phụ đề", "subtitleFile": "Tệp phụ đề (VTT, SRT, ASS, SSA)", @@ -774,8 +852,8 @@ "payments": "Thanh toán" }, "stats": { - "total": "{count} thông báo", - "unread": "{count} chưa đọc" + "total": "{{count}} thông báo", + "unread": "{{count}} chưa đọc" }, "actions": { "markAllRead": "Đánh dấu đã đọc tất cả", @@ -796,9 +874,9 @@ "subtitle": "Bạn đã xem hết! Hãy quay lại sau." }, "time": { - "minutesAgo": "{count} phút trước", - "hoursAgo": "{count} giờ trước", - "daysAgo": "{count} ngày trước" + "minutesAgo": "{{count}} phút trước", + "hoursAgo": "{{count}} giờ trước", + "daysAgo": "{{count}} ngày trước" }, "mocks": { "videoProcessed": { @@ -830,23 +908,23 @@ "upload": { "dialog": { "title": "Tải video lên", - "subtitle": "Thêm tối đa {maxItems} video mỗi đợt", + "subtitle": "Thêm tối đa {{maxItems}} video mỗi đợt", "mode": { "local": "Tệp cục bộ", "remote": "URL từ xa" }, "queueFullTitle": "Hàng đợi đã đầy", - "queueFullDescription": "Tối đa {maxItems} video mỗi đợt. Hãy bắt đầu hoặc xóa hàng đợi hiện tại trước.", - "slotsRemaining": "Còn {remaining} / {maxItems} vị trí", + "queueFullDescription": "Tối đa {{maxItems}} video mỗi đợt. Hãy bắt đầu hoặc xóa hàng đợi hiện tại trước.", + "slotsRemaining": "Còn {{remaining}} / {{maxItems}} vị trí", "formatsHint": "MP4, MOV, MKV · tối đa 10 GB mỗi tệp", "close": "Đóng", - "startUpload": "Bắt đầu tải ({count})", + "startUpload": "Bắt đầu tải ({{count}})", "duplicateFilesSummary": "Đã bỏ qua tệp trùng lặp", - "duplicateFilesDetailOne": "{count} tệp đã có trong hàng đợi.", - "duplicateFilesDetailOther": "{count} tệp đã có trong hàng đợi.", + "duplicateFilesDetailOne": "{{count}} tệp đã có trong hàng đợi.", + "duplicateFilesDetailOther": "{{count}} tệp đã có trong hàng đợi.", "duplicateUrlsSummary": "Đã bỏ qua URL trùng lặp", - "duplicateUrlsDetailOne": "{count} URL đã có trong hàng đợi.", - "duplicateUrlsDetailOther": "{count} URL đã có trong hàng đợi." + "duplicateUrlsDetailOne": "{{count}} URL đã có trong hàng đợi.", + "duplicateUrlsDetailOther": "{{count}} URL đã có trong hàng đợi." }, "dropzone": { "releaseToAdd": "Thả để thêm", @@ -869,7 +947,7 @@ "status": { "pending": "Chờ tải", "uploading": "Đang tải lên...", - "uploadingThreads": "Đang tải · {threads} luồng", + "uploadingThreads": "Đang tải · {{threads}} luồng", "processing": "Đang xử lý...", "complete": "Hoàn tất", "error": "Thất bại", @@ -882,7 +960,7 @@ }, "bulkActions": { "title": "Thiết lập nhanh", - "applyToPending": "Áp dụng cho {count} tệp đang chờ", + "applyToPending": "Áp dụng cho {{count}} tệp đang chờ", "selectCategory": "Chọn danh mục...", "category": { "learning": "Học tập", @@ -895,15 +973,15 @@ }, "indicator": { "allDone": "Hoàn tất", - "uploading": "Đang tải lên {count} tệp...", - "waiting": "{count} tệp đang chờ", - "completeProgress": "Hoàn tất {complete} / {total}", + "uploading": "Đang tải lên {{count}} tệp...", + "waiting": "{{count}} tệp đang chờ", + "completeProgress": "Hoàn tất {{complete}} / {{total}}", "start": "Bắt đầu", "viewVideos": "Xem video", "addMoreFiles": "Thêm tệp" }, "errors": { - "chunkUploadFailed": "Không thể tải phần {index}", + "chunkUploadFailed": "Không thể tải phần {{index}}", "mergeFailed": "Gộp tệp thất bại" } }, diff --git a/src/api/client.ts b/src/api/client.ts index b9753f1..e358ca8 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -9,7 +9,112 @@ * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## * --------------------------------------------------------------- */ -import { customFetch } from "@httpClientAdapter"; + +import { customFetch } from '@httpClientAdapter'; + +export interface AdminCreateAdminPaymentRequest { + payment_method: string; + plan_id: string; + term_months: number; + topup_amount?: number; + user_id: string; +} + +export interface AdminCreateAdminUserRequest { + email: string; + /** @minLength 6 */ + password: string; + plan_id?: string; + role?: string; + username?: string; +} + +export interface AdminDashboardPayload { + active_subscriptions?: number; + new_users_today?: number; + new_videos_today?: number; + total_ad_templates?: number; + total_payments?: number; + total_revenue?: number; + total_storage_used?: number; + total_users?: number; + total_videos?: number; +} + +export interface AdminSaveAdminAdTemplateRequest { + ad_format?: string; + description?: string; + duration?: number; + is_active?: boolean; + is_default?: boolean; + name: string; + user_id: string; + vast_tag_url: string; +} + +export interface AdminSaveAdminVideoRequest { + ad_template_id?: string; + description?: string; + duration?: number; + format?: string; + size: number; + status?: string; + title: string; + url: string; + user_id: string; +} + +export interface AdminSavePlanRequest { + cycle: string; + description?: string; + features?: string[]; + is_active?: boolean; + name: string; + price: number; + storage_limit: number; + upload_limit: number; +} + +export interface AdminUpdateAdminPaymentRequest { + status: string; +} + +export interface AdminUpdateAdminUserRequest { + email?: string; + password?: string; + plan_id?: string; + role?: string; + username?: string; +} + +export interface AdminUpdateUserRoleRequest { + role: string; +} + +export interface AdtemplatesSaveAdTemplateRequest { + ad_format?: string; + description?: string; + duration?: number; + is_active?: boolean; + is_default?: boolean; + name: string; + vast_tag_url: string; +} + +export interface AdtemplatesTemplateListPayload { + templates?: ManualAdTemplate[]; +} + +export interface AdtemplatesTemplatePayload { + template?: ManualAdTemplate; +} + +export interface AuthChangePasswordRequest { + current_password: string; + /** @minLength 6 */ + new_password: string; +} + export interface AuthForgotPasswordRequest { email: string; } @@ -32,11 +137,57 @@ export interface AuthResetPasswordRequest { token: string; } +export interface AuthUpdateMeRequest { + email?: string; + language?: string; + locale?: string; + username?: string; +} + +export interface AuthUserPayload { + avatar?: string; + created_at?: string; + email?: string; + google_id?: string; + id?: string; + language?: string; + locale?: string; + plan_expires_at?: string; + plan_expiring_soon?: boolean; + plan_id?: string; + plan_payment_method?: string; + plan_started_at?: string; + plan_term_months?: number; + role?: string; + storage_used?: number; + updated_at?: string; + username?: string; + wallet_balance?: number; +} + +export interface DomainsCreateDomainRequest { + name: string; +} + +export interface ManualAdTemplate { + ad_format?: string; + created_at?: string; + description?: string; + duration?: number; + id?: string; + is_active?: boolean; + is_default?: boolean; + name?: string; + updated_at?: string; + user_id?: string; + vast_tag_url?: string; +} + export interface ModelPlan { cycle?: string; description?: string; duration_limit?: number; - features?: string; + features?: string[]; id?: string; is_active?: boolean; name?: string; @@ -46,20 +197,6 @@ export interface ModelPlan { upload_limit?: number; } -export interface ModelUser { - avatar?: string; - created_at?: string; - email?: string; - google_id?: string; - id?: string; - password?: string; - plan_id?: string; - role?: string; - storage_used?: number; - updated_at?: string; - username?: string; -} - export interface ModelVideo { created_at?: string; description?: string; @@ -82,15 +219,44 @@ export interface ModelVideo { } export interface PaymentCreatePaymentRequest { - amount: number; + payment_method: string; plan_id: string; + term_months: number; + topup_amount?: number; +} + +export interface PaymentTopupWalletRequest { + amount: number; +} + +export interface PreferencesSettingsPreferencesRequest { + airplay?: boolean; + autoplay?: boolean; + chromecast?: boolean; + email_notifications?: boolean; + language?: string; + locale?: string; + loop?: boolean; + marketing_notifications?: boolean; + muted?: boolean; + pip?: boolean; + push_notifications?: boolean; + show_controls?: boolean; + telegram_notifications?: boolean; } export interface ResponseResponse { code?: number; + data?: any; message?: string; } +export interface UsageUsagePayload { + total_storage?: number; + total_videos?: number; + user_id?: string; +} + export interface VideoCreateVideoRequest { description?: string; /** Maybe client knows, or we process later */ @@ -102,6 +268,12 @@ export interface VideoCreateVideoRequest { url: string; } +export interface VideoUpdateVideoRequest { + ad_template_id?: string; + description?: string; + title: string; +} + export interface VideoUploadURLRequest { content_type: string; filename: string; @@ -161,7 +333,7 @@ export enum ContentType { } export class HttpClient { - public baseUrl: string = ""; + public baseUrl: string = "//localhost:8080"; private securityData: SecurityDataType | null = null; private securityWorker?: ApiConfig["securityWorker"]; private abortControllers = new Map(); @@ -329,7 +501,7 @@ export class HttpClient { body: typeof body === "undefined" || body === null ? null - : payloadFormatter(body) + : payloadFormatter(body), }, ).then(async (response) => { const r = response as HttpResponse; @@ -340,18 +512,18 @@ export class HttpClient { const data = !responseFormat ? r : await responseToParse[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data; - } else { - r.error = data; - } - return r; - }) - .catch((e) => { - r.error = e; - return r; - }); + .then((data) => { + if (r.ok) { + r.data = data; + } else { + r.error = data; + } + return r; + }) + .catch((e) => { + r.error = e; + return r; + }); if (cancelToken) { this.abortControllers.delete(cancelToken); @@ -368,6 +540,7 @@ export class HttpClient { * @version 1.0 * @license Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html) * @termsOfService http://swagger.io/terms/ + * @baseUrl //localhost:8080 * @contact API Support (http://www.swagger.io/support) * * This is the API server for Stream application. @@ -375,7 +548,721 @@ export class HttpClient { export class Api< SecurityDataType extends unknown, > extends HttpClient { + adTemplates = { + /** + * @description Get all VAST ad templates for the current user + * + * @tags ad-templates + * @name AdTemplatesList + * @summary List Ad Templates + * @request GET:/ad-templates + * @secure + */ + adTemplatesList: (params: RequestParams = {}) => + this.request< + ResponseResponse & { + data?: AdtemplatesTemplateListPayload; + }, + ResponseResponse + >({ + path: `/ad-templates`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Create a VAST ad template for the current user + * + * @tags ad-templates + * @name AdTemplatesCreate + * @summary Create Ad Template + * @request POST:/ad-templates + * @secure + */ + adTemplatesCreate: ( + request: AdtemplatesSaveAdTemplateRequest, + params: RequestParams = {}, + ) => + this.request< + ResponseResponse & { + data?: AdtemplatesTemplatePayload; + }, + ResponseResponse + >({ + path: `/ad-templates`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Update a VAST ad template for the current user + * + * @tags ad-templates + * @name AdTemplatesUpdate + * @summary Update Ad Template + * @request PUT:/ad-templates/{id} + * @secure + */ + adTemplatesUpdate: ( + id: string, + request: AdtemplatesSaveAdTemplateRequest, + params: RequestParams = {}, + ) => + this.request< + ResponseResponse & { + data?: AdtemplatesTemplatePayload; + }, + ResponseResponse + >({ + path: `/ad-templates/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Delete a VAST ad template for the current user + * + * @tags ad-templates + * @name AdTemplatesDelete + * @summary Delete Ad Template + * @request DELETE:/ad-templates/{id} + * @secure + */ + adTemplatesDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/ad-templates/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + }; + admin = { + /** + * @description Get paginated list of all ad templates across users (admin only) + * + * @tags admin + * @name AdTemplatesList + * @summary List All Ad Templates + * @request GET:/admin/ad-templates + * @secure + */ + adTemplatesList: ( + query?: { + /** + * Page + * @default 1 + */ + page?: number; + /** + * Limit + * @default 20 + */ + limit?: number; + /** Filter by user ID */ + user_id?: string; + /** Search by name */ + search?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/ad-templates`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * @description Create an ad template for any user (admin only) + * + * @tags admin + * @name AdTemplatesCreate + * @summary Create Ad Template + * @request POST:/admin/ad-templates + * @secure + */ + adTemplatesCreate: ( + request: AdminSaveAdminAdTemplateRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/ad-templates`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Get ad template detail (admin only) + * + * @tags admin + * @name AdTemplatesDetail + * @summary Get Ad Template Detail + * @request GET:/admin/ad-templates/{id} + * @secure + */ + adTemplatesDetail: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/ad-templates/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Update an ad template for any user (admin only) + * + * @tags admin + * @name AdTemplatesUpdate + * @summary Update Ad Template + * @request PUT:/admin/ad-templates/{id} + * @secure + */ + adTemplatesUpdate: ( + id: string, + request: AdminSaveAdminAdTemplateRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/ad-templates/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Delete any ad template by ID (admin only) + * + * @tags admin + * @name AdTemplatesDelete + * @summary Delete Ad Template (Admin) + * @request DELETE:/admin/ad-templates/{id} + * @secure + */ + adTemplatesDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/ad-templates/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Get system-wide statistics for the admin dashboard + * + * @tags admin + * @name DashboardList + * @summary Admin Dashboard + * @request GET:/admin/dashboard + * @secure + */ + dashboardList: (params: RequestParams = {}) => + this.request< + ResponseResponse & { + data?: AdminDashboardPayload; + }, + ResponseResponse + >({ + path: `/admin/dashboard`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Get paginated list of all payments across users (admin only) + * + * @tags admin + * @name PaymentsList + * @summary List All Payments + * @request GET:/admin/payments + * @secure + */ + paymentsList: ( + query?: { + /** + * Page + * @default 1 + */ + page?: number; + /** + * Limit + * @default 20 + */ + limit?: number; + /** Filter by user ID */ + user_id?: string; + /** Filter by status */ + status?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/payments`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * @description Create a manual subscription charge for a user (admin only) + * + * @tags admin + * @name PaymentsCreate + * @summary Create Payment + * @request POST:/admin/payments + * @secure + */ + paymentsCreate: ( + request: AdminCreateAdminPaymentRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/payments`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Get payment detail (admin only) + * + * @tags admin + * @name PaymentsDetail + * @summary Get Payment Detail + * @request GET:/admin/payments/{id} + * @secure + */ + paymentsDetail: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/payments/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Update payment status safely without hard delete (admin only) + * + * @tags admin + * @name PaymentsUpdate + * @summary Update Payment + * @request PUT:/admin/payments/{id} + * @secure + */ + paymentsUpdate: ( + id: string, + request: AdminUpdateAdminPaymentRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/payments/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Get all plans with usage counts (admin only) + * + * @tags admin + * @name PlansList + * @summary List Plans + * @request GET:/admin/plans + * @secure + */ + plansList: (params: RequestParams = {}) => + this.request({ + path: `/admin/plans`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Create a plan (admin only) + * + * @tags admin + * @name PlansCreate + * @summary Create Plan + * @request POST:/admin/plans + * @secure + */ + plansCreate: (request: AdminSavePlanRequest, params: RequestParams = {}) => + this.request({ + path: `/admin/plans`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Update a plan (admin only) + * + * @tags admin + * @name PlansUpdate + * @summary Update Plan + * @request PUT:/admin/plans/{id} + * @secure + */ + plansUpdate: ( + id: string, + request: AdminSavePlanRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/plans/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Delete a plan, or deactivate it if already used (admin only) + * + * @tags admin + * @name PlansDelete + * @summary Delete Plan + * @request DELETE:/admin/plans/{id} + * @secure + */ + plansDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/plans/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Get paginated list of all users (admin only) + * + * @tags admin + * @name UsersList + * @summary List Users + * @request GET:/admin/users + * @secure + */ + usersList: ( + query?: { + /** + * Page + * @default 1 + */ + page?: number; + /** + * Limit + * @default 20 + */ + limit?: number; + /** Search by email or username */ + search?: string; + /** Filter by role */ + role?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/users`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * @description Create a user from admin panel (admin only) + * + * @tags admin + * @name UsersCreate + * @summary Create User + * @request POST:/admin/users + * @secure + */ + usersCreate: ( + request: AdminCreateAdminUserRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/users`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Get detailed info about a single user (admin only) + * + * @tags admin + * @name UsersDetail + * @summary Get User Detail + * @request GET:/admin/users/{id} + * @secure + */ + usersDetail: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/users/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Update a user from admin panel (admin only) + * + * @tags admin + * @name UsersUpdate + * @summary Update User + * @request PUT:/admin/users/{id} + * @secure + */ + usersUpdate: ( + id: string, + request: AdminUpdateAdminUserRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/users/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Delete a user and their data (admin only) + * + * @tags admin + * @name UsersDelete + * @summary Delete User + * @request DELETE:/admin/users/{id} + * @secure + */ + usersDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/users/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Change user role (admin only). Valid: USER, ADMIN, BLOCK + * + * @tags admin + * @name UsersRoleUpdate + * @summary Update User Role + * @request PUT:/admin/users/{id}/role + * @secure + */ + usersRoleUpdate: ( + id: string, + request: AdminUpdateUserRoleRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/users/${id}/role`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Get paginated list of all videos across users (admin only) + * + * @tags admin + * @name VideosList + * @summary List All Videos + * @request GET:/admin/videos + * @secure + */ + videosList: ( + query?: { + /** + * Page + * @default 1 + */ + page?: number; + /** + * Limit + * @default 20 + */ + limit?: number; + /** Search by title */ + search?: string; + /** Filter by user ID */ + user_id?: string; + /** Filter by status */ + status?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/videos`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * @description Create a manual video record for a user (admin only) + * + * @tags admin + * @name VideosCreate + * @summary Create Video + * @request POST:/admin/videos + * @secure + */ + videosCreate: ( + request: AdminSaveAdminVideoRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/videos`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Get video detail by ID (admin only) + * + * @tags admin + * @name VideosDetail + * @summary Get Video Detail + * @request GET:/admin/videos/{id} + * @secure + */ + videosDetail: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/videos/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Update video metadata and status (admin only) + * + * @tags admin + * @name VideosUpdate + * @summary Update Video + * @request PUT:/admin/videos/{id} + * @secure + */ + videosUpdate: ( + id: string, + request: AdminSaveAdminVideoRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/admin/videos/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Delete any video by ID (admin only) + * + * @tags admin + * @name VideosDelete + * @summary Delete Video (Admin) + * @request DELETE:/admin/videos/{id} + * @secure + */ + videosDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/admin/videos/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + }; auth = { + /** + * @description Change the authenticated user's local password + * + * @tags auth + * @name ChangePasswordCreate + * @summary Change Password + * @request POST:/auth/change-password + * @secure + */ + changePasswordCreate: ( + request: AuthChangePasswordRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/auth/change-password`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + /** * @description Request password reset link * @@ -408,12 +1295,13 @@ export class Api< googleCallbackList: (params: RequestParams = {}) => this.request< ResponseResponse & { - data?: ModelUser; + data?: AuthUserPayload; }, ResponseResponse >({ path: `/auth/google/callback`, method: "GET", + format: "json", ...params, }), @@ -426,7 +1314,7 @@ export class Api< * @request GET:/auth/google/login */ googleLoginList: (params: RequestParams = {}) => - this.request({ + this.request({ path: `/auth/google/login`, method: "GET", ...params, @@ -443,7 +1331,7 @@ export class Api< loginCreate: (request: AuthLoginRequest, params: RequestParams = {}) => this.request< ResponseResponse & { - data?: ModelUser; + data?: AuthUserPayload; }, ResponseResponse >({ @@ -462,12 +1350,13 @@ export class Api< * @name LogoutCreate * @summary Logout * @request POST:/auth/logout + * @secure */ logoutCreate: (params: RequestParams = {}) => - this.request({ + this.request({ path: `/auth/logout`, method: "POST", - type: ContentType.Json, + secure: true, format: "json", ...params, }), @@ -514,9 +1403,237 @@ export class Api< ...params, }), }; + domains = { + /** + * @description Get all whitelisted domains for the current user + * + * @tags domains + * @name DomainsList + * @summary List Domains + * @request GET:/domains + * @secure + */ + domainsList: (params: RequestParams = {}) => + this.request({ + path: `/domains`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Add a domain to the current user's whitelist + * + * @tags domains + * @name DomainsCreate + * @summary Create Domain + * @request POST:/domains + * @secure + */ + domainsCreate: ( + request: DomainsCreateDomainRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/domains`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Remove a domain from the current user's whitelist + * + * @tags domains + * @name DomainsDelete + * @summary Delete Domain + * @request DELETE:/domains/{id} + * @secure + */ + domainsDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/domains/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + }; + me = { + /** + * @description Get the authenticated user's profile payload + * + * @tags auth + * @name GetMe + * @summary Get Current User + * @request GET:/me + * @secure + */ + getMe: (params: RequestParams = {}) => + this.request({ + path: `/me`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Update the authenticated user's profile information + * + * @tags auth + * @name PutMe + * @summary Update Current User + * @request PUT:/me + * @secure + */ + putMe: (request: AuthUpdateMeRequest, params: RequestParams = {}) => + this.request({ + path: `/me`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Permanently delete the authenticated user's account and related data + * + * @tags auth + * @name DeleteMe + * @summary Delete My Account + * @request DELETE:/me + * @secure + */ + deleteMe: (params: RequestParams = {}) => + this.request({ + path: `/me`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Remove videos and settings-related resources for the authenticated user + * + * @tags auth + * @name ClearDataCreate + * @summary Clear My Data + * @request POST:/me/clear-data + * @secure + */ + clearDataCreate: (params: RequestParams = {}) => + this.request({ + path: `/me/clear-data`, + method: "POST", + secure: true, + format: "json", + ...params, + }), + }; + notifications = { + /** + * @description Get notifications for the current user + * + * @tags notifications + * @name NotificationsList + * @summary List Notifications + * @request GET:/notifications + * @secure + */ + notificationsList: (params: RequestParams = {}) => + this.request({ + path: `/notifications`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Delete all notifications for the current user + * + * @tags notifications + * @name NotificationsDelete + * @summary Clear Notifications + * @request DELETE:/notifications + * @secure + */ + notificationsDelete: (params: RequestParams = {}) => + this.request({ + path: `/notifications`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Mark all notifications as read for the current user + * + * @tags notifications + * @name ReadAllCreate + * @summary Mark All Notifications Read + * @request POST:/notifications/read-all + * @secure + */ + readAllCreate: (params: RequestParams = {}) => + this.request({ + path: `/notifications/read-all`, + method: "POST", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Delete a single notification for the current user + * + * @tags notifications + * @name NotificationsDelete2 + * @summary Delete Notification + * @request DELETE:/notifications/{id} + * @originalName notificationsDelete + * @duplicate + * @secure + */ + notificationsDelete2: (id: string, params: RequestParams = {}) => + this.request({ + path: `/notifications/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Mark a single notification as read for the current user + * + * @tags notifications + * @name ReadCreate + * @summary Mark Notification Read + * @request POST:/notifications/{id}/read + * @secure + */ + readCreate: (id: string, params: RequestParams = {}) => + this.request({ + path: `/notifications/${id}/read`, + method: "POST", + secure: true, + format: "json", + ...params, + }), + }; payments = { /** - * @description Create a new payment + * @description Create a new payment for buying or renewing a plan * * @tags payment * @name PaymentsCreate @@ -537,6 +1654,41 @@ export class Api< format: "json", ...params, }), + + /** + * @description Get payment history for the current user + * + * @tags payment + * @name HistoryList + * @summary List Payment History + * @request GET:/payments/history + * @secure + */ + historyList: (params: RequestParams = {}) => + this.request({ + path: `/payments/history`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Download invoice text for a payment or wallet top-up + * + * @tags payment + * @name InvoiceList + * @summary Download Invoice + * @request GET:/payments/{id}/invoice + * @secure + */ + invoiceList: (id: string, params: RequestParams = {}) => + this.request({ + path: `/payments/${id}/invoice`, + method: "GET", + secure: true, + ...params, + }), }; plans = { /** @@ -551,9 +1703,7 @@ export class Api< plansList: (params: RequestParams = {}) => this.request< ResponseResponse & { - data: { - plans: ModelPlan[]; - } + data?: ModelPlan[]; }, ResponseResponse >({ @@ -564,6 +1714,72 @@ export class Api< ...params, }), }; + settings = { + /** + * @description Get notification, player, and locale preferences for the current user + * + * @tags settings + * @name PreferencesList + * @summary Get Preferences + * @request GET:/settings/preferences + * @secure + */ + preferencesList: (params: RequestParams = {}) => + this.request({ + path: `/settings/preferences`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * @description Update notification, player, and locale preferences for the current user + * + * @tags settings + * @name PreferencesUpdate + * @summary Update Preferences + * @request PUT:/settings/preferences + * @secure + */ + preferencesUpdate: ( + request: PreferencesSettingsPreferencesRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/settings/preferences`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + }; + usage = { + /** + * @description Get the authenticated user's total video count and total storage usage + * + * @tags usage + * @name UsageList + * @summary Get Usage + * @request GET:/usage + * @secure + */ + usageList: (params: RequestParams = {}) => + this.request< + ResponseResponse & { + data?: UsageUsagePayload; + }, + ResponseResponse + >({ + path: `/usage`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + }; videos = { /** * @description Get paginated videos @@ -589,14 +1805,7 @@ export class Api< }, params: RequestParams = {}, ) => - this.request({ + this.request({ path: `/videos`, method: "GET", query: query, @@ -678,11 +1887,76 @@ export class Api< format: "json", ...params, }), + + /** + * @description Update title and description for a video owned by the current user + * + * @tags video + * @name VideosUpdate + * @summary Update Video + * @request PUT:/videos/{id} + * @secure + */ + videosUpdate: ( + id: string, + request: VideoUpdateVideoRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/videos/${id}`, + method: "PUT", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * @description Delete a video owned by the current user + * + * @tags video + * @name VideosDelete + * @summary Delete Video + * @request DELETE:/videos/{id} + * @secure + */ + videosDelete: (id: string, params: RequestParams = {}) => + this.request({ + path: `/videos/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params, + }), + }; + wallet = { + /** + * @description Add funds to wallet balance for the current user + * + * @tags payment + * @name TopupsCreate + * @summary Top Up Wallet + * @request POST:/wallet/topups + * @secure + */ + topupsCreate: ( + request: PaymentTopupWalletRequest, + params: RequestParams = {}, + ) => + this.request({ + path: `/wallet/topups`, + method: "POST", + body: request, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), }; } export const client = new Api({ - baseUrl: 'r', - // baseUrl: 'https://api.pipic.fun', - customFetch -}); \ No newline at end of file + baseUrl: '/r', + customFetch, +}); diff --git a/src/api/httpClientAdapter.client.ts b/src/api/httpClientAdapter.client.ts index 3095c53..bf6abaa 100644 --- a/src/api/httpClientAdapter.client.ts +++ b/src/api/httpClientAdapter.client.ts @@ -1,6 +1,6 @@ -export const customFetch = (url: string, options: RequestInit) => { - return fetch(url, { - ...options, - credentials: "include", +export const customFetch: typeof fetch = (input, init) => { + return fetch(input, { + ...init, + credentials: 'include', }); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/AppTopLoadingBar.vue b/src/components/AppTopLoadingBar.vue new file mode 100644 index 0000000..7b098c8 --- /dev/null +++ b/src/components/AppTopLoadingBar.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/NotificationDrawer.vue b/src/components/NotificationDrawer.vue index d69ebe7..d8c4e82 100644 --- a/src/components/NotificationDrawer.vue +++ b/src/components/NotificationDrawer.vue @@ -1,130 +1,51 @@ diff --git a/src/components/icons/Bell.vue b/src/components/icons/Bell.vue index d8a3d6f..89292fc 100644 --- a/src/components/icons/Bell.vue +++ b/src/components/icons/Bell.vue @@ -1,5 +1,5 @@ + + diff --git a/src/routes/auth/layout.vue b/src/routes/auth/layout.vue index b4fcfec..bb1c2d8 100644 --- a/src/routes/auth/layout.vue +++ b/src/routes/auth/layout.vue @@ -45,6 +45,11 @@ const content = computed(() => ({ title: t('auth.layout.forgot.title'), subtitle: t('auth.layout.forgot.subtitle'), headTitle: t('auth.layout.forgot.headTitle') + }, + 'google-auth-finalize': { + title: 'Google sign in', + subtitle: 'Completing your Google sign in.', + headTitle: 'Google sign in - Holistream' } })); diff --git a/src/routes/index.ts b/src/routes/index.ts index 225e977..e02fe98 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,4 @@ +import { useRouteLoading } from "@/composables/useRouteLoading"; import { useAuthStore } from "@/stores/auth"; import { headSymbol, type ReactiveHead, type ResolvableValue } from "@unhead/vue"; import { inject } from "vue"; @@ -68,6 +69,11 @@ const routes: RouteData[] = [ name: "forgot", component: () => import("./auth/forgot.vue"), }, + { + path: "auth/google/finalize", + name: "google-auth-finalize", + component: () => import("./auth/google-finalize.vue"), + }, ], }, { @@ -85,16 +91,6 @@ const routes: RouteData[] = [ }, }, }, - // { - // path: "upload", - // name: "upload", - // component: () => import("./upload/Upload.vue"), - // meta: { - // head: { - // title: "Upload - Holistream", - // }, - // }, - // }, { path: "videos", children: [ @@ -114,16 +110,6 @@ const routes: RouteData[] = [ }, }, }, - // { - // path: ":id", - // name: "video-detail", - // component: () => import("./video/DetailVideo.vue"), - // meta: { - // head: { - // title: "Edit Video - Holistream", - // }, - // }, - // }, ], }, { @@ -255,16 +241,27 @@ const createAppRouter = () => { }, }); + const loading = useRouteLoading() router.beforeEach((to, from) => { const auth = useAuthStore(); const head = inject(headSymbol); - (head as any).push(to.meta.head || {}); + (head as any).push(to.meta.head || {}); + if (to.fullPath !== from.fullPath && !import.meta.env.SSR) { + loading.start() + } if (to.matched.some((record) => record.meta.requiresAuth)) { if (!auth.user) { return { name: "login" }; } } }); + router.afterEach(() => { + loading.finish() + }) + + router.onError(() => { + loading.fail() + }) return router; }; diff --git a/src/routes/notification/Notification.vue b/src/routes/notification/Notification.vue index a3a8e3d..f559986 100644 --- a/src/routes/notification/Notification.vue +++ b/src/routes/notification/Notification.vue @@ -1,117 +1,49 @@ @@ -128,8 +60,8 @@ const handleClearAll = () => {
{ diff --git a/src/routes/overview/Overview.vue b/src/routes/overview/Overview.vue index b3894b4..ecdc42a 100644 --- a/src/routes/overview/Overview.vue +++ b/src/routes/overview/Overview.vue @@ -1,48 +1,42 @@ diff --git a/src/routes/overview/components/RecentVideos.vue b/src/routes/overview/components/RecentVideos.vue index e4291c4..8ef5b60 100644 --- a/src/routes/overview/components/RecentVideos.vue +++ b/src/routes/overview/components/RecentVideos.vue @@ -4,6 +4,7 @@ import EmptyState from '@/components/dashboard/EmptyState.vue'; import { formatDate, formatDuration } from '@/lib/utils'; import { useTranslation } from 'i18next-vue'; import { useRouter } from 'vue-router'; +import { useUIState } from '@/stores/uiState'; interface Props { loading: boolean; @@ -13,6 +14,7 @@ interface Props { defineProps(); const router = useRouter(); +const uiState = useUIState(); const { t } = useTranslation(); const getStatusClass = (status?: string) => { @@ -48,7 +50,7 @@ const getStatusClass = (status?: string) => {

{{ t('overview.recentVideos.title') }}

- {{ t('overview.recentVideos.viewAll') }} @@ -58,7 +60,7 @@ const getStatusClass = (status?: string) => { + :onAction="() => uiState.toggleUploadDialog()" />
diff --git a/src/routes/overview/components/StatsOverview.vue b/src/routes/overview/components/StatsOverview.vue index 94d5844..76b2e04 100644 --- a/src/routes/overview/components/StatsOverview.vue +++ b/src/routes/overview/components/StatsOverview.vue @@ -11,7 +11,6 @@ interface Props { totalViews: number; storageUsed: number; storageLimit: number; - uploadsThisMonth: number; }; } @@ -21,8 +20,8 @@ const localeTag = computed(() => i18next.resolvedLanguage === 'vi' ? 'vi-VN' : ' diff --git a/src/routes/settings/AdsVast/AdsVast.vue b/src/routes/settings/AdsVast/AdsVast.vue index 91887d3..df1571c 100644 --- a/src/routes/settings/AdsVast/AdsVast.vue +++ b/src/routes/settings/AdsVast/AdsVast.vue @@ -1,4 +1,5 @@ diff --git a/src/routes/settings/SecurityNConnected/SecurityNConnected.vue b/src/routes/settings/SecurityNConnected/SecurityNConnected.vue index 596257b..4192927 100644 --- a/src/routes/settings/SecurityNConnected/SecurityNConnected.vue +++ b/src/routes/settings/SecurityNConnected/SecurityNConnected.vue @@ -19,10 +19,10 @@ import { computed, ref } from 'vue'; const auth = useAuthStore(); const toast = useAppToast(); const confirm = useAppConfirm(); -const { t } = useTranslation(); +const { t, i18next } = useTranslation(); const languageSaving = ref(false); -const selectedLanguage = ref('en'); +const selectedLanguage = ref(auth.user?.language || "en"); const languageOptions = computed(() => supportedLocales.map((value) => ({ value, label: t(`settings.securityConnected.language.options.${value}`) @@ -282,7 +282,7 @@ const disconnectTelegram = async () => { - { - - - - - - { + + + + + + +import { cn } from '@/lib/utils'; + +const props = withDefaults(defineProps<{ + actionClass?: string; + titleClass?: string; + descriptionClass?: string; +}>(), { + actionClass: 'h-6 w-11', + titleClass: 'w-32', + descriptionClass: 'w-56 max-w-full', +}); + + + diff --git a/src/routes/settings/components/SettingsTableSkeleton.vue b/src/routes/settings/components/SettingsTableSkeleton.vue new file mode 100644 index 0000000..931e381 --- /dev/null +++ b/src/routes/settings/components/SettingsTableSkeleton.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/routes/settings/components/billing/BillingHistorySection.vue b/src/routes/settings/components/billing/BillingHistorySection.vue index 2f5186b..56f5359 100644 --- a/src/routes/settings/components/billing/BillingHistorySection.vue +++ b/src/routes/settings/components/billing/BillingHistorySection.vue @@ -8,12 +8,17 @@ type PaymentHistoryItem = { plan: string; status: string; invoiceId: string; + currency: string; + kind: string; + details?: string[]; }; defineProps<{ title: string; description: string; items: PaymentHistoryItem[]; + loading?: boolean; + downloadingId?: string | null; formatMoney: (amount: number) => string; getStatusStyles: (status: string) => string; getStatusLabel: (status: string) => string; @@ -52,42 +57,58 @@ const emit = defineEmits<{
{{ invoiceLabel }}
-
+
+
+
+
+
+
+
+
+
+ +

{{ emptyLabel }}

-
-
-

{{ item.date }}

+
diff --git a/src/routes/settings/components/billing/BillingPlansSection.vue b/src/routes/settings/components/billing/BillingPlansSection.vue index 9b4a1c5..050fab1 100644 --- a/src/routes/settings/components/billing/BillingPlansSection.vue +++ b/src/routes/settings/components/billing/BillingPlansSection.vue @@ -9,18 +9,18 @@ defineProps<{ isLoading: boolean; plans: ModelPlan[]; currentPlanId?: string; - subscribing: string | null; + selectingPlanId?: string | null; formatMoney: (amount: number) => string; getPlanStorageText: (plan: ModelPlan) => string; getPlanDurationText: (plan: ModelPlan) => string; getPlanUploadsText: (plan: ModelPlan) => string; currentPlanLabel: string; - processingLabel: string; - upgradeLabel: string; + selectingLabel: string; + chooseLabel: string; }>(); const emit = defineEmits<{ - (e: 'subscribe', plan: ModelPlan): void; + (e: 'select', plan: ModelPlan): void; }>(); @@ -44,22 +44,32 @@ const emit = defineEmits<{
-

{{ plan.name }}

+
+

{{ plan.name }}

+ + {{ currentPlanLabel }} + +

{{ plan.description }}

{{ formatMoney(plan.price || 0) }} - /{{ plan.cycle }} + / {{ $t('settings.billing.cycle.'+plan.cycle) }}
-
    -
  • + +
  • + + {{ feature }}
diff --git a/src/routes/settings/components/billing/BillingWalletRow.vue b/src/routes/settings/components/billing/BillingWalletRow.vue index 403537a..52e02ff 100644 --- a/src/routes/settings/components/billing/BillingWalletRow.vue +++ b/src/routes/settings/components/billing/BillingWalletRow.vue @@ -8,6 +8,9 @@ defineProps<{ title: string; description: string; buttonLabel: string; + subscriptionTitle?: string; + subscriptionDescription?: string; + subscriptionTone?: 'default' | 'warning'; }>(); const emit = defineEmits<{ @@ -22,12 +25,25 @@ const emit = defineEmits<{ diff --git a/src/routes/video/CopyVideoModal.vue b/src/routes/video/CopyVideoModal.vue index 92af199..4834872 100644 --- a/src/routes/video/CopyVideoModal.vue +++ b/src/routes/video/CopyVideoModal.vue @@ -1,6 +1,5 @@ - - diff --git a/src/routes/video/DetailVideoModal.vue b/src/routes/video/DetailVideoModal.vue index 85814db..7a951b2 100644 --- a/src/routes/video/DetailVideoModal.vue +++ b/src/routes/video/DetailVideoModal.vue @@ -1,8 +1,8 @@ diff --git a/src/routes/video/components/Detail/VideoInfoHeader.vue b/src/routes/video/components/Detail/VideoInfoHeader.vue deleted file mode 100644 index b744ceb..0000000 --- a/src/routes/video/components/Detail/VideoInfoHeader.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - diff --git a/src/routes/video/components/Detail/VideoInfoPanel.vue b/src/routes/video/components/Detail/VideoInfoPanel.vue deleted file mode 100644 index 40d22db..0000000 --- a/src/routes/video/components/Detail/VideoInfoPanel.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - diff --git a/src/routes/video/components/Detail/VideoPlayer.vue b/src/routes/video/components/Detail/VideoPlayer.vue deleted file mode 100644 index 4ff3656..0000000 --- a/src/routes/video/components/Detail/VideoPlayer.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - diff --git a/src/routes/video/components/Detail/VideoSkeleton.vue b/src/routes/video/components/Detail/VideoSkeleton.vue deleted file mode 100644 index c3f28d8..0000000 --- a/src/routes/video/components/Detail/VideoSkeleton.vue +++ /dev/null @@ -1,44 +0,0 @@ - diff --git a/src/server/routes/display.ts b/src/server/routes/display.ts index f142084..db3c196 100644 --- a/src/server/routes/display.ts +++ b/src/server/routes/display.ts @@ -1,23 +1,50 @@ import type { Hono } from 'hono'; -import { saveImageFromStream } from '../modules/merge'; +import { getManifest, saveImageFromStream, streamManifest } from '../modules/merge'; + +const guessContentType = (filename: string) => { + const lower = filename.toLowerCase(); + if (lower.endsWith('.mp4')) return 'video/mp4'; + if (lower.endsWith('.webm')) return 'video/webm'; + if (lower.endsWith('.mov')) return 'video/quicktime'; + if (lower.endsWith('.mkv')) return 'video/x-matroska'; + if (lower.endsWith('.m3u8')) return 'application/vnd.apple.mpegurl'; + return 'application/octet-stream'; +}; + +const buildStreamResponse = async (id: string) => { + const manifest = await getManifest(id); + if (!manifest) { + return new Response(JSON.stringify({ error: 'Manifest not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(streamManifest(manifest), { + status: 200, + headers: { + 'Content-Type': guessContentType(manifest.filename), + 'Cache-Control': 'public, max-age=3600', + 'Content-Disposition': `inline; filename="${manifest.filename}"`, + }, + }); +}; export function registerDisplayRoutes(app: Hono) { - // app.get('/manifest/:id', async (c) => { - // const manifest = await getListFiles(); - // if (!manifest) { - // return c.json({ error: "Manifest not found" }, 404); - // } - // return c.json(manifest); - // }); + app.get('/display/:id', async (c) => buildStreamResponse(c.req.param('id'))); + app.get('/play/index/:id', async (c) => buildStreamResponse(c.req.param('id'))); + app.put('/display/:id/thumbnail', async (c) => { const arrayBuffer = await c.req.arrayBuffer(); - await saveImageFromStream(arrayBuffer, crypto.randomUUID()); + await saveImageFromStream(arrayBuffer, c.req.param('id')); return c.body('ok'); - // nhận rawData, lưu vào storage, cập nhật url thumbnail vào database - }); - app.put('/display/:id/metadata', async (c) => { + app.put('/display/:id/metadata', async (c) => { + return c.json({ status: 'not_implemented' }, 501); + }); + + app.post('/display/:id/subs', async (c) => { + return c.json({ status: 'not_implemented' }, 501); }); - app.post('/display/:id/subs', async (c) => {}); } diff --git a/src/server/routes/manifest.ts b/src/server/routes/manifest.ts index d04c415..5fc4257 100644 --- a/src/server/routes/manifest.ts +++ b/src/server/routes/manifest.ts @@ -1,11 +1,11 @@ -import { getListFiles } from '@/server/modules/merge'; +import { getManifest } from '@/server/modules/merge'; import type { Hono } from 'hono'; export function registerManifestRoutes(app: Hono) { app.get('/manifest/:id', async (c) => { - const manifest = await getListFiles(); + const manifest = await getManifest(c.req.param('id')); if (!manifest) { - return c.json({ error: "Manifest not found" }, 404); + return c.json({ error: 'Manifest not found' }, 404); } return c.json(manifest); }); diff --git a/src/server/routes/merge.ts b/src/server/routes/merge.ts index 11cba75..ac56ce8 100644 --- a/src/server/routes/merge.ts +++ b/src/server/routes/merge.ts @@ -48,6 +48,9 @@ export function registerMergeRoutes(app: Hono) { filename: manifest.filename, total_parts: manifest.total_parts, size: manifest.size, + playback_url: `/display/${manifest.id}`, + play_url: `/play/index/${manifest.id}`, + manifest_url: `/manifest/${manifest.id}`, }); } catch (e: any) { return c.json({ error: e?.message ?? String(e) }, 500); diff --git a/src/server/routes/ssr.ts b/src/server/routes/ssr.ts index 33d5a0f..55bdba5 100644 --- a/src/server/routes/ssr.ts +++ b/src/server/routes/ssr.ts @@ -33,7 +33,7 @@ export function registerSSRRoutes(app: Hono) { const appStream = renderToWebStream(vueApp, ctx); // HTML Head - await stream.write(``); + await stream.write(``); await stream.write(""); // SSR Head tags @@ -63,7 +63,7 @@ export function registerSSRRoutes(app: Hono) { Object.assign(ctx, { $p: pinia.state.value, $colada: serializeQueryCache(queryCache), - $locale: lang, + $locale: auth.user?.language ?? lang, }); // App data script diff --git a/src/stores/auth.ts b/src/stores/auth.ts index b81c716..775967b 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,4 +1,4 @@ -import { client, type ModelUser, type ResponseResponse } from '@/api/client'; +import { client, type AuthUserPayload, type ResponseResponse } from '@/api/client'; import { TinyMqttClient } from '@/lib/liteMqtt'; import { useTranslation } from 'i18next-vue'; import { defineStore } from 'pinia'; @@ -7,18 +7,17 @@ import { useRouter } from 'vue-router'; type ProfileUpdatePayload = { username?: string; - email?: string; language?: string; locale?: string; }; type AuthResponseBody = ResponseResponse & { - data?: ModelUser | { user?: ModelUser }; + data?: AuthUserPayload | { user?: AuthUserPayload }; }; const mqttBrokerUrl = 'wss://mqtt-dashboard.com:8884/mqtt'; -const extractUser = (body?: AuthResponseBody | null): ModelUser | null => { +const extractUser = (body?: AuthResponseBody | null): AuthUserPayload | null => { const data = body?.data; if (!data) return null; @@ -26,7 +25,7 @@ const extractUser = (body?: AuthResponseBody | null): ModelUser | null => { return data.user; } - return data as ModelUser; + return data as AuthUserPayload; }; const getGoogleLoginPath = () => { @@ -35,9 +34,9 @@ const getGoogleLoginPath = () => { }; export const useAuthStore = defineStore('auth', () => { - const user = ref(null); + const user = ref(null); const router = useRouter(); - const { t } = useTranslation(); + const { t, i18next } = useTranslation(); const loading = ref(false); const error = ref(null); const initialized = ref(false); @@ -61,7 +60,6 @@ export const useAuthStore = defineStore('auth', () => { clearMqttClient(); if (!userId) return; - mqttClient = new TinyMqttClient( mqttBrokerUrl, [['ecos1231231', userId, '#'].join('/')], @@ -71,21 +69,21 @@ export const useAuthStore = defineStore('auth', () => { ); mqttClient.connect(); }); + watch(() => user.value?.language, (lng) => i18next.changeLanguage(lng)) + async function fetchMe() { + const response = await client.me.getMe({ baseUrl: '/r' }); + + const nextUser = extractUser(response.data as AuthResponseBody); + user.value = nextUser; + i18next.changeLanguage(nextUser?.language) + return nextUser; + } async function init() { if (initialized.value) return; try { - const response = await client.request({ - path: '/me', - method: 'GET', - format: 'json', - }); - - const nextUser = extractUser(response.data as AuthResponseBody); - if (nextUser) { - user.value = nextUser; - } + await fetchMe(); } catch { user.value = null; } finally { @@ -149,16 +147,11 @@ export const useAuthStore = defineStore('auth', () => { error.value = null; try { - const response = await client.request({ - path: '/me', - method: 'PUT', - body: data, - format: 'json', - }); + const response = await client.me.putMe(data, { baseUrl: '/r' }); const nextUser = extractUser(response.data as AuthResponseBody); if (nextUser) { - user.value = { ...(user.value ?? {}), ...nextUser } as ModelUser; + user.value = { ...(user.value ?? {}), ...nextUser } as AuthUserPayload; } return true; @@ -189,15 +182,10 @@ export const useAuthStore = defineStore('auth', () => { error.value = null; try { - await client.request({ - path: '/auth/change-password', - method: 'POST', - body: { - current_password: currentPassword, - new_password: newPassword, - }, - format: 'json', - }); + await client.auth.changePasswordCreate({ + current_password: currentPassword, + new_password: newPassword, + }, { baseUrl: '/r' }); return true; } catch (e: any) { console.error('Change password error', e); @@ -230,6 +218,7 @@ export const useAuthStore = defineStore('auth', () => { error, initialized, init, + fetchMe, login, loginWithGoogle, register, diff --git a/src/type.d.ts b/src/type.d.ts index c7cbac6..814a535 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -1,6 +1,12 @@ /// /// +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent<{}, {}, any>; + export default component; +} + declare module "@httpClientAdapter" { - export const customFetch: (url: string, options: RequestInit) => Promise; + export const customFetch: typeof fetch; } \ No newline at end of file diff --git a/ssrPlugin.ts b/ssrPlugin.ts index 26394be..5c654f8 100644 --- a/ssrPlugin.ts +++ b/ssrPlugin.ts @@ -32,10 +32,11 @@ export function clientFirstBuild(): Plugin { // Client build first // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (clientEnvironment) { - // console.log("Client First Build Plugin: Building client...", clientEnvironment.resolve); + // clientEnvironment.config.build.outDir = "dist/client"; + // console.log("Client First Build Plugin: Building client...", Object.keys()); await builder.build(clientEnvironment); } - + // console.log("Client First Build Plugin: Client build complete.", workerEnvironments); // Then worker builds for (const workerEnv of workerEnvironments) { await builder.build(workerEnv); @@ -109,23 +110,14 @@ export default function ssrPlugin(): Plugin[] { config.define = config.define || {}; }, resolveId(id, importer, options) { - if (!['@httpClientAdapter', '@liteMqtt'].includes(id)) return - switch (id) { - case '@httpClientAdapter': + if (!id.startsWith('@httpClientAdapter')) return + const pwd = process.cwd() return path.resolve( __dirname, options?.ssr - ? "./src/api/httpClientAdapter.server.ts" - : "./src/api/httpClientAdapter.client.ts" + ? pwd+"/src/api/httpClientAdapter.server.ts" + : pwd+"/src/api/httpClientAdapter.client.ts" ); - case '@liteMqtt': - return path.resolve( - __dirname, - options?.ssr - ? "./src/lib/liteMqtt.server.ts" - : "./src/lib/liteMqtt.ts" - ); - } }, async configResolved(config) { const viteConfig = config as any; @@ -143,7 +135,8 @@ export default function ssrPlugin(): Plugin[] { const clientBuild = viteConfig.environments.client.build; clientBuild.manifest = true; clientBuild.rollupOptions = clientBuild.rollupOptions || {}; - clientBuild.rollupOptions.input = "src/client.ts"; + // clientBuild.rollupOptions.input = "src/client.ts"; + // clientBuild.outDir = "dist/client"; if (!viteConfig.environments.ssr) { const manifestPath = path.join(clientBuild.outDir as string, '.vite/manifest.json') try { @@ -159,4 +152,4 @@ export default function ssrPlugin(): Plugin[] { plugins.push(injectManifest()); return plugins; -} +} \ No newline at end of file diff --git a/uno.config.ts b/uno.config.ts index 8fd2d83..d8ee46d 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -27,13 +27,13 @@ export default defineConfig({ theme: { colors: { primary: { - DEFAULT: "#14a74b", + DEFAULT: "#4563ca", 50: "#effcf3", 100: "#dcf9e2", 200: "#bbf0c8", 300: "#86efac", 400: "#4ade80", - 500: "#14a74b", + 500: "#4563ca", 600: "#16a34a", 700: "#15803d", 800: "#166534", @@ -178,10 +178,6 @@ export default defineConfig({ DEFAULT: "#fafafa", light: "#f8f9fa", }, - muted: { - DEFAULT: "#f5f4f2", - light: "#f8f9fa", - }, border: { DEFAULT: "#e6e7e2", light: "#f8f9fa", diff --git a/vite-plugin-ssr-middleware.ts b/vite-plugin-ssr-middleware.ts new file mode 100644 index 0000000..2567868 --- /dev/null +++ b/vite-plugin-ssr-middleware.ts @@ -0,0 +1,118 @@ +import type { Connect, Plugin } from "vite"; +import { name as packageName } from "./package.json"; +import { createMiddleware } from "@hattip/adapter-node"; +import { pathToFileURL } from "url"; + + +export function vitePluginSsrMiddleware({ + entry, + preview, + mode = "ssrLoadModule", +}: { + entry: string; + preview?: string; + mode?: "ssrLoadModule" | "ModuleRunner" | "ModuleRunner-HMR"; +}): Plugin { + return { + name: packageName, + + apply(config, env) { + // skip client build + return Boolean(env.command === "serve" || config.build?.ssr); + }, + + config(config, env) { + if (env.command === "serve") { + return { + // disable builtin HTML middleware, which would rewrite `req.url` to "/index.html" + appType: "custom", + }; + } + if (env.command === "build" && config.build?.ssr) { + return { + build: { + rollupOptions: { + input: { + index: entry, + }, + }, + }, + }; + } + return; + }, + + async configureServer(server) { + let loadModule = server.ssrLoadModule; + if (mode === "ModuleRunner" || mode === "ModuleRunner-HMR") { + const { createServerModuleRunner } = await import("vite"); + const runner = createServerModuleRunner(server.environments.ssr, { + hmr: mode === "ModuleRunner-HMR" ? undefined : false, + }); + loadModule = (id: string) => runner.import(id); + } +// const mod = await loadModule(entry); + const handler: Connect.NextHandleFunction = async (req, res, next) => { + console.log("vite-plugin-ssr-middleware handling request:", req.method, req.url); + // expose ViteDevServer via request + Object.defineProperty(req, "viteDevServer", { value: server }); + + try { + const mod = await loadModule(entry); + // console.log("preview module loaded:", mod); + await createMiddleware((ctx) => mod["default"].fetch(ctx.request))(req, res, next); + // await mod["default"](req, res, next); + } catch (e) { + next(e); + } + }; + return () => server.middlewares.use(handler); + }, + + async configurePreviewServer(server) { + if (preview) { + const mod = await import( pathToFileURL(preview).href); + return () => server.middlewares.use(createMiddleware((ctx) => mod["default"].fetch(ctx.request))); + } + return; + }, + }; +} + +// minimal logger inspired by +// https://github.com/koajs/logger +// https://github.com/honojs/hono/blob/25beca878f2662fedd84ed3fbf80c6a515609cea/src/middleware/logger/index.ts + +export function vitePluginLogger(): Plugin { + return { + name: vitePluginLogger.name, + configureServer(server) { + return () => server.middlewares.use(loggerMiddleware()); + }, + configurePreviewServer(server) { + return () => server.middlewares.use(loggerMiddleware()); + }, + }; +} + +function loggerMiddleware(): Connect.NextHandleFunction { + return (req, res, next) => { + const url = new URL(req.originalUrl!, "https://test.local"); + console.log(" -->", req.method, url.pathname); + const startTime = Date.now(); + res.once("close", () => { + console.log( + " <--", + req.method, + url.pathname, + res.statusCode, + formatDuration(Date.now() - startTime), + ); + }); + next(); + }; +} + +function formatDuration(ms: number) { + return ms < 1000 ? `${Math.floor(ms)}ms` : `${(ms / 1000).toFixed(1)}s`; +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 1151185..9a3599a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,3 @@ -import { cloudflare } from "@cloudflare/vite-plugin"; import vue from "@vitejs/plugin-vue"; import vueJsx from "@vitejs/plugin-vue-jsx"; import path from "node:path"; @@ -7,12 +6,13 @@ import Components from "unplugin-vue-components/vite"; import AutoImport from "unplugin-auto-import/vite"; import { defineConfig } from "vite"; import ssrPlugin from "./ssrPlugin"; +import { vitePluginSsrMiddleware } from "./vite-plugin-ssr-middleware"; export default defineConfig((env) => { // console.log("env:", env, import.meta.env); return { server: { - host: '0.0.0.0' - }, + host: '0.0.0.0' + }, plugins: [ unocss(), vue(), @@ -29,8 +29,30 @@ export default defineConfig((env) => { directives: false, }), ssrPlugin(), - cloudflare(), + vitePluginSsrMiddleware({ + entry: "/src/index.tsx", + preview: path.resolve("dist/server/index.js"), + }), ], + environments: { + client: { + build: { + outDir: "dist/client", + rollupOptions: { + input: { index: "/src/client.ts" }, + }, + }, + }, + server: { + build: { + outDir: "dist/server", + copyPublicDir: false, + rollupOptions: { + input: { index: "/src/index.tsx" }, + }, + }, + }, + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), @@ -41,8 +63,8 @@ export default defineConfig((env) => { exclude: ["vue"], }, - ssr: { - noExternal: ["vue"], - }, + // ssr: { + // noExternal: ["vue"], + // }, }; });