From a3fd03d32a99feb0ea45a08ea0afaedf697af79d Mon Sep 17 00:00:00 2001
From: lonelyhentxi
Date: Tue, 3 Jun 2025 02:21:49 +0800
Subject: [PATCH] refactor: refactor subscriptions
---
Cargo.lock | 187 +-
apps/recorder/src/extract/mikan/constants.rs | 2 +
apps/recorder/src/extract/mikan/mod.rs | 6 +-
apps/recorder/src/extract/mikan/web.rs | 15 +-
apps/webui/package.json | 1 -
apps/webui/src/components/layout/nav-main.tsx | 6 +-
.../src/components/ui/form-field-errors.tsx | 60 +-
apps/webui/src/components/ui/form.tsx | 164 -
apps/webui/src/domains/recorder/context.ts | 16 +
.../src/domains/recorder/schema/mikan.ts | 192 ++
.../domains/recorder/schema/subscriptions.ts | 68 +-
.../recorder/services/mikan.service.ts | 6 +
.../recorder/services/subscription.service.ts | 31 +
apps/webui/src/infra/errors/arktype.ts | 15 +
apps/webui/src/infra/errors/common.ts | 6 +
apps/webui/src/infra/graphql/gql/gql.ts | 24 +-
apps/webui/src/infra/graphql/gql/graphql.ts | 24 +-
apps/webui/src/main.tsx | 6 +-
apps/webui/src/presentation/routeTree.gen.ts | 92 +-
.../routes/_app/credential3rd/create.tsx | 47 +-
.../routes/_app/credential3rd/detail.$id.tsx | 2 +-
.../routes/_app/credential3rd/edit.$id.tsx | 4 +-
.../routes/_app/credential3rd/manage.tsx | 27 +-
.../subscriptions/-credential3rd-select.tsx | 93 +
.../routes/_app/subscriptions/create.tsx | 488 ++-
...ail.$subscriptionId.tsx => detail.$id.tsx} | 8 +-
...{edit.$subscriptionId.tsx => edit.$id.tsx} | 7 +-
.../routes/_app/subscriptions/manage.tsx | 71 +-
package.json | 9 +-
pnpm-lock.yaml | 2969 +++++++++--------
30 files changed, 2612 insertions(+), 2034 deletions(-)
delete mode 100644 apps/webui/src/components/ui/form.tsx
create mode 100644 apps/webui/src/domains/recorder/context.ts
create mode 100644 apps/webui/src/domains/recorder/schema/mikan.ts
create mode 100644 apps/webui/src/domains/recorder/services/mikan.service.ts
create mode 100644 apps/webui/src/domains/recorder/services/subscription.service.ts
create mode 100644 apps/webui/src/infra/errors/arktype.ts
create mode 100644 apps/webui/src/presentation/routes/_app/subscriptions/-credential3rd-select.tsx
rename apps/webui/src/presentation/routes/_app/subscriptions/{detail.$subscriptionId.tsx => detail.$id.tsx} (81%)
rename apps/webui/src/presentation/routes/_app/subscriptions/{edit.$subscriptionId.tsx => edit.$id.tsx} (63%)
diff --git a/Cargo.lock b/Cargo.lock
index 283fd0f..610bc69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -139,9 +139,9 @@ dependencies = [
[[package]]
name = "anstream"
-version = "0.6.18"
+version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -154,33 +154,33 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.10"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
-version = "3.0.8"
+version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
@@ -611,9 +611,9 @@ dependencies = [
[[package]]
name = "backon"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496"
+checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7"
dependencies = [
"fastrand",
"gloo-timers",
@@ -664,9 +664,9 @@ dependencies = [
[[package]]
name = "base64ct"
-version = "1.7.3"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bigdecimal"
@@ -934,9 +934,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.24"
+version = "1.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
+checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
dependencies = [
"jobserver",
"libc",
@@ -1095,9 +1095,9 @@ dependencies = [
[[package]]
name = "color-eyre"
-version = "0.6.4"
+version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e1761c0e16f8883bbbb8ce5990867f4f06bf11a0253da6495a04ce4b6ef0ec"
+checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
dependencies = [
"backtrace",
"color-spantrace",
@@ -1110,9 +1110,9 @@ dependencies = [
[[package]]
name = "color-spantrace"
-version = "0.2.2"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ddd8d5bfda1e11a501d0a7303f3bfed9aa632ebdb859be40d0fd70478ed70d5"
+checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
dependencies = [
"once_cell",
"owo-colors",
@@ -1122,9 +1122,9 @@ dependencies = [
[[package]]
name = "colorchoice"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "colored"
@@ -1365,7 +1365,7 @@ dependencies = [
"crossterm_winapi",
"libc",
"mio 0.8.11",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -1566,7 +1566,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
- "parking_lot_core 0.9.10",
+ "parking_lot_core 0.9.11",
]
[[package]]
@@ -1580,7 +1580,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
- "parking_lot_core 0.9.10",
+ "parking_lot_core 0.9.11",
"serde",
]
@@ -2211,7 +2211,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
]
[[package]]
@@ -2453,7 +2453,7 @@ dependencies = [
"getrandom 0.3.3",
"no-std-compat",
"nonzero_ext",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"portable-atomic",
"quanta",
"rand 0.9.1",
@@ -2811,9 +2811,9 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.13"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
+checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3399,7 +3399,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a396bb213c2d09ed6c5495fd082c991b6ab39c9daf4fff59e6727f85c73e4c5"
dependencies = [
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"pin-project-lite",
"tokio",
]
@@ -3463,7 +3463,7 @@ dependencies = [
"memmap2 0.9.5",
"mime_guess",
"notify",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"rand 0.8.5",
"regex",
"reqwest",
@@ -3530,7 +3530,7 @@ dependencies = [
"librqbit-bencode",
"librqbit-buffers",
"librqbit-clone-to-owned",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"serde",
"tokio",
"tokio-util",
@@ -3557,7 +3557,7 @@ dependencies = [
"librqbit-bencode",
"librqbit-clone-to-owned",
"librqbit-core",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"rand 0.8.5",
"serde",
"serde_json",
@@ -3606,7 +3606,7 @@ dependencies = [
"librqbit-bencode",
"librqbit-buffers",
"librqbit-core",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"rand 0.8.5",
"reqwest",
"serde",
@@ -3705,9 +3705,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
-version = "0.4.12"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
@@ -3977,7 +3977,7 @@ dependencies = [
"event-listener",
"futures-util",
"loom 0.7.2",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"portable-atomic",
"rustc_version",
"smallvec",
@@ -4512,12 +4512,12 @@ dependencies = [
[[package]]
name = "parking_lot"
-version = "0.12.3"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
- "parking_lot_core 0.9.10",
+ "parking_lot_core 0.9.11",
]
[[package]]
@@ -4536,9 +4536,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
-version = "0.9.10"
+version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
@@ -5376,9 +5376,9 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.12.18"
+version = "0.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
+checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -6224,7 +6224,7 @@ dependencies = [
"futures",
"log",
"once_cell",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"scc",
"serial_test_derive",
]
@@ -6731,7 +6731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"phf_shared",
"precomputed-hash",
"serde",
@@ -7116,7 +7116,7 @@ dependencies = [
"bytes",
"libc",
"mio 1.0.4",
- "parking_lot 0.12.3",
+ "parking_lot 0.12.4",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -7289,9 +7289,9 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.4"
+version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"async-compression",
"bitflags 2.9.1",
@@ -7996,7 +7996,7 @@ dependencies = [
"windows-interface",
"windows-link",
"windows-result",
- "windows-strings 0.4.2",
+ "windows-strings",
]
[[package]]
@@ -8050,13 +8050,13 @@ dependencies = [
[[package]]
name = "windows-registry"
-version = "0.4.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
+checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
dependencies = [
+ "windows-link",
"windows-result",
- "windows-strings 0.3.1",
- "windows-targets 0.53.0",
+ "windows-strings",
]
[[package]]
@@ -8068,15 +8068,6 @@ dependencies = [
"windows-link",
]
-[[package]]
-name = "windows-strings"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
-dependencies = [
- "windows-link",
-]
-
[[package]]
name = "windows-strings"
version = "0.4.2"
@@ -8137,29 +8128,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
+ "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
-[[package]]
-name = "windows-targets"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
-dependencies = [
- "windows_aarch64_gnullvm 0.53.0",
- "windows_aarch64_msvc 0.53.0",
- "windows_i686_gnu 0.53.0",
- "windows_i686_gnullvm 0.53.0",
- "windows_i686_msvc 0.53.0",
- "windows_x86_64_gnu 0.53.0",
- "windows_x86_64_gnullvm 0.53.0",
- "windows_x86_64_msvc 0.53.0",
-]
-
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -8181,12 +8156,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
-
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -8199,12 +8168,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
-
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -8217,24 +8180,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-[[package]]
-name = "windows_i686_gnu"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
-
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
-
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -8247,12 +8198,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-[[package]]
-name = "windows_i686_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
-
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -8265,12 +8210,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
-
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -8283,12 +8222,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
-
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -8301,12 +8234,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
-
[[package]]
name = "winnow"
version = "0.7.10"
@@ -8565,9 +8492,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
-version = "0.4.14"
+version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
+checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974"
dependencies = [
"zune-core",
]
diff --git a/apps/recorder/src/extract/mikan/constants.rs b/apps/recorder/src/extract/mikan/constants.rs
index 78549f1..83ba0a6 100644
--- a/apps/recorder/src/extract/mikan/constants.rs
+++ b/apps/recorder/src/extract/mikan/constants.rs
@@ -15,3 +15,5 @@ pub const MIKAN_BANGUMI_RSS_PATH: &str = "/RSS/Bangumi";
pub const MIKAN_BANGUMI_ID_QUERY_KEY: &str = "bangumiId";
pub const MIKAN_FANSUB_ID_QUERY_KEY: &str = "subgroupid";
pub const MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY: &str = "token";
+pub const MIKAN_SEASON_STR_QUERY_KEY: &str = "seasonStr";
+pub const MIKAN_YEAR_QUERY_KEY: &str = "year";
diff --git a/apps/recorder/src/extract/mikan/mod.rs b/apps/recorder/src/extract/mikan/mod.rs
index 7483a87..f892faf 100644
--- a/apps/recorder/src/extract/mikan/mod.rs
+++ b/apps/recorder/src/extract/mikan/mod.rs
@@ -12,9 +12,9 @@ pub use constants::{
MIKAN_BANGUMI_HOMEPAGE_PATH, MIKAN_BANGUMI_ID_QUERY_KEY, MIKAN_BANGUMI_POSTER_PATH,
MIKAN_BANGUMI_RSS_PATH, MIKAN_EPISODE_HOMEPAGE_PATH, MIKAN_EPISODE_TORRENT_PATH,
MIKAN_FANSUB_ID_QUERY_KEY, MIKAN_LOGIN_PAGE_PATH, MIKAN_LOGIN_PAGE_SEARCH,
- MIKAN_POSTER_BUCKET_KEY, MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH,
- MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY, MIKAN_UNKNOWN_FANSUB_ID,
- MIKAN_UNKNOWN_FANSUB_NAME,
+ MIKAN_POSTER_BUCKET_KEY, MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_SEASON_STR_QUERY_KEY,
+ MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
+ MIKAN_UNKNOWN_FANSUB_ID, MIKAN_UNKNOWN_FANSUB_NAME, MIKAN_YEAR_QUERY_KEY,
};
pub use credential::MikanCredentialForm;
pub use subscription::{
diff --git a/apps/recorder/src/extract/mikan/web.rs b/apps/recorder/src/extract/mikan/web.rs
index 46c9b33..11901bc 100644
--- a/apps/recorder/src/extract/mikan/web.rs
+++ b/apps/recorder/src/extract/mikan/web.rs
@@ -23,8 +23,9 @@ use crate::{
MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH, MIKAN_BANGUMI_HOMEPAGE_PATH,
MIKAN_BANGUMI_ID_QUERY_KEY, MIKAN_BANGUMI_POSTER_PATH, MIKAN_BANGUMI_RSS_PATH,
MIKAN_EPISODE_HOMEPAGE_PATH, MIKAN_FANSUB_ID_QUERY_KEY, MIKAN_POSTER_BUCKET_KEY,
- MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH,
- MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY, MikanClient,
+ MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_SEASON_STR_QUERY_KEY,
+ MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH, MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
+ MIKAN_YEAR_QUERY_KEY, MikanClient,
},
},
storage::{StorageContentCategory, StorageServiceTrait},
@@ -421,10 +422,10 @@ impl MikanSeasonFlowUrlMeta {
if url.path().starts_with(MIKAN_SEASON_FLOW_PAGE_PATH) {
if let (Some(year), Some(season_str)) = (
url.query_pairs()
- .find(|(key, _)| key == "year")
+ .find(|(key, _)| key == MIKAN_YEAR_QUERY_KEY)
.and_then(|(_, value)| value.parse::().ok()),
url.query_pairs()
- .find(|(key, _)| key == "seasonStr")
+ .find(|(key, _)| key == MIKAN_SEASON_STR_QUERY_KEY)
.and_then(|(_, value)| MikanSeasonStr::from_str(&value).ok()),
) {
Some(Self { year, season_str })
@@ -455,8 +456,8 @@ pub fn build_mikan_season_flow_url(
let mut url = mikan_base_url;
url.set_path(MIKAN_SEASON_FLOW_PAGE_PATH);
url.query_pairs_mut()
- .append_pair("year", &year.to_string())
- .append_pair("seasonStr", &season_str.to_string());
+ .append_pair(MIKAN_YEAR_QUERY_KEY, &year.to_string())
+ .append_pair(MIKAN_SEASON_STR_QUERY_KEY, &season_str.to_string());
url
}
@@ -467,7 +468,7 @@ pub fn build_mikan_bangumi_expand_subscribed_url(
let mut url = mikan_base_url;
url.set_path(MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH);
url.query_pairs_mut()
- .append_pair("bangumiId", mikan_bangumi_id)
+ .append_pair(MIKAN_BANGUMI_ID_QUERY_KEY, mikan_bangumi_id)
.append_pair("showSubscribed", "true");
url
}
diff --git a/apps/webui/package.json b/apps/webui/package.json
index c1bf8a6..1e0c64d 100644
--- a/apps/webui/package.json
+++ b/apps/webui/package.json
@@ -70,7 +70,6 @@
"react": "^19.1.0",
"react-day-picker": "9.6.0",
"react-dom": "^19.1.0",
- "react-hook-form": "^7.56.3",
"react-resizable-panels": "^3.0.1",
"recharts": "^2.15.3",
"rxjs": "^7.8.2",
diff --git a/apps/webui/src/components/layout/nav-main.tsx b/apps/webui/src/components/layout/nav-main.tsx
index 1afa4a5..475de0b 100644
--- a/apps/webui/src/components/layout/nav-main.tsx
+++ b/apps/webui/src/components/layout/nav-main.tsx
@@ -66,9 +66,9 @@ export function NavMain({
);
};
- const renderCollapsedSubMenu = (item: NavMainItem) => {
+ const renderCollapsedSubMenu = (item: NavMainItem, itemIndex: number) => {
return (
-
+
diff --git a/apps/webui/src/components/ui/form-field-errors.tsx b/apps/webui/src/components/ui/form-field-errors.tsx
index 2490a98..b467124 100644
--- a/apps/webui/src/components/ui/form-field-errors.tsx
+++ b/apps/webui/src/components/ui/form-field-errors.tsx
@@ -7,47 +7,51 @@ interface ErrorDisplayProps {
| string
| StandardSchemaV1Issue
| Array;
+ isDirty: boolean;
+ submissionAttempts: number;
}
-export function FormFieldErrors({ errors }: ErrorDisplayProps) {
+export function FormFieldErrors({
+ errors,
+ isDirty,
+ submissionAttempts,
+}: ErrorDisplayProps) {
const errorList = useMemo(
() =>
- (Array.isArray(errors) ? errors : [errors]).filter(Boolean) as Array<
- string | StandardSchemaV1Issue
- >,
+ Array.from(
+ new Set(
+ (Array.isArray(errors) ? errors : [errors])
+ .map((e) => {
+ if (typeof e === "string") {
+ return e;
+ }
+ if (e?.message) {
+ return e.message;
+ }
+ return null;
+ })
+ .filter(Boolean) as string[]
+ )
+ ),
[errors]
);
+ if (!isDirty && !(submissionAttempts > 0)) {
+ return null;
+ }
+
if (!errorList.length) {
return null;
}
return (
- {errorList.map((error, index) => {
- if (typeof error === "string") {
- return (
- -
-
- {error}
-
- );
- }
- return (
- -
-
-
- );
- })}
+ {errorList.map((error, index) => (
+ -
+
+ {error}
+
+ ))}
);
}
diff --git a/apps/webui/src/components/ui/form.tsx b/apps/webui/src/components/ui/form.tsx
deleted file mode 100644
index 0205944..0000000
--- a/apps/webui/src/components/ui/form.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import * as LabelPrimitive from "@radix-ui/react-label";
-import { Slot } from "@radix-ui/react-slot";
-import * as React from "react";
-import {
- Controller,
- type ControllerProps,
- type FieldPath,
- type FieldValues,
- FormProvider,
- useFormContext,
- useFormState,
-} from "react-hook-form";
-
-import { Label } from "@/components/ui/label";
-import { cn } from "@/presentation/utils";
-
-const Form = FormProvider;
-
-type FormFieldContextValue<
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath,
-> = {
- name: TName;
-};
-
-const FormFieldContext = React.createContext(
- {} as FormFieldContextValue
-);
-
-const FormField = <
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath,
->({
- ...props
-}: ControllerProps) => {
- return (
-
-
-
- );
-};
-
-const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext);
- const itemContext = React.useContext(FormItemContext);
- const { getFieldState } = useFormContext();
- const formState = useFormState({ name: fieldContext.name });
- const fieldState = getFieldState(fieldContext.name, formState);
-
- if (!fieldContext) {
- throw new Error("useFormField should be used within ");
- }
-
- const { id } = itemContext;
-
- return {
- id,
- name: fieldContext.name,
- formItemId: `${id}-form-item`,
- formDescriptionId: `${id}-form-item-description`,
- formMessageId: `${id}-form-item-message`,
- ...fieldState,
- };
-};
-
-type FormItemContextValue = {
- id: string;
-};
-
-const FormItemContext = React.createContext(
- {} as FormItemContextValue
-);
-
-function FormItem({ className, ...props }: React.ComponentProps<"div">) {
- const id = React.useId();
-
- return (
-
-
-
- );
-}
-
-function FormLabel({
- className,
- ...props
-}: React.ComponentProps) {
- const { error, formItemId } = useFormField();
-
- return (
-
- );
-}
-
-function FormControl({ ...props }: React.ComponentProps) {
- const { error, formItemId, formDescriptionId, formMessageId } =
- useFormField();
-
- return (
-
- );
-}
-
-function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
- const { formDescriptionId } = useFormField();
-
- return (
-
- );
-}
-
-function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
- const { error, formMessageId } = useFormField();
- const body = error ? String(error?.message ?? "") : props.children;
-
- if (!body) {
- return null;
- }
-
- return (
-
- {body}
-
- );
-}
-
-export {
- useFormField,
- Form,
- FormItem,
- FormLabel,
- FormControl,
- FormDescription,
- FormMessage,
- FormField,
-};
diff --git a/apps/webui/src/domains/recorder/context.ts b/apps/webui/src/domains/recorder/context.ts
new file mode 100644
index 0000000..c4a305c
--- /dev/null
+++ b/apps/webui/src/domains/recorder/context.ts
@@ -0,0 +1,16 @@
+import type { Provider } from '@outposts/injection-js';
+import { MikanService } from './services/mikan.service';
+import { SubscriptionService } from './services/subscription.service';
+
+export function provideRecorder(): Provider[] {
+ return [
+ {
+ provide: MikanService,
+ useClass: MikanService,
+ },
+ {
+ provide: SubscriptionService,
+ useClass: SubscriptionService,
+ },
+ ];
+}
diff --git a/apps/webui/src/domains/recorder/schema/mikan.ts b/apps/webui/src/domains/recorder/schema/mikan.ts
new file mode 100644
index 0000000..88443b1
--- /dev/null
+++ b/apps/webui/src/domains/recorder/schema/mikan.ts
@@ -0,0 +1,192 @@
+import { UnimplementedError } from '@/infra/errors/common';
+import { SubscriptionCategoryEnum } from '@/infra/graphql/gql/graphql';
+import { type ArkErrors, type } from 'arktype';
+
+export const MIKAN_UNKNOWN_FANSUB_NAME = '生肉/不明字幕';
+export const MIKAN_UNKNOWN_FANSUB_ID = '202';
+export const MIKAN_ACCOUNT_MANAGE_PAGE_PATH = '/Account/Manage';
+export const MIKAN_SEASON_FLOW_PAGE_PATH = '/Home/BangumiCoverFlow';
+export const MIKAN_BANGUMI_HOMEPAGE_PATH = '/Home/Bangumi';
+export const MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH = '/Home/ExpandBangumi';
+export const MIKAN_EPISODE_HOMEPAGE_PATH = '/Home/Episode';
+export const MIKAN_BANGUMI_POSTER_PATH = '/images/Bangumi';
+export const MIKAN_EPISODE_TORRENT_PATH = '/Download';
+export const MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH = '/RSS/MyBangumi';
+export const MIKAN_BANGUMI_RSS_PATH = '/RSS/Bangumi';
+export const MIKAN_BANGUMI_ID_QUERY_KEY = 'bangumiId';
+export const MIKAN_FANSUB_ID_QUERY_KEY = 'subgroupid';
+export const MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY = 'token';
+export const MIKAN_SEASON_STR_QUERY_KEY = 'seasonStr';
+export const MIKAN_YEAR_QUERY_KEY = 'year';
+
+export const MikanSubscriptionCategoryEnum = {
+ MikanBangumi: SubscriptionCategoryEnum.MikanBangumi,
+ MikanSeason: SubscriptionCategoryEnum.MikanSeason,
+ MikanSubscriber: SubscriptionCategoryEnum.MikanSubscriber,
+} as const;
+
+export type MikanSubscriptionCategoryEnum =
+ (typeof MikanSubscriptionCategoryEnum)[keyof typeof MikanSubscriptionCategoryEnum];
+
+export const MikanSeasonEnum = {
+ Spring: '春',
+ Summer: '夏',
+ Autumn: '秋',
+ Winter: '冬',
+} as const;
+
+export type MikanSeasonEnum =
+ (typeof MikanSeasonEnum)[keyof typeof MikanSeasonEnum];
+
+export const MikanSeasonSchema = type.enumerated(
+ MikanSeasonEnum.Spring,
+ MikanSeasonEnum.Summer,
+ MikanSeasonEnum.Autumn,
+ MikanSeasonEnum.Winter
+);
+
+export const MikanSubscriptionBangumiSourceUrlSchema = type({
+ category: `'${SubscriptionCategoryEnum.MikanBangumi}'`,
+ mikanBangumiId: 'string>0',
+ mikanFansubId: 'string>0',
+});
+
+export type MikanSubscriptionBangumiSourceUrl =
+ typeof MikanSubscriptionBangumiSourceUrlSchema.infer;
+
+export const MikanSubscriptionSeasonSourceUrlSchema = type({
+ category: `'${SubscriptionCategoryEnum.MikanSeason}'`,
+ seasonStr: MikanSeasonSchema,
+ year: 'number>0',
+});
+
+export type MikanSubscriptionSeasonSourceUrl =
+ typeof MikanSubscriptionSeasonSourceUrlSchema.infer;
+
+export const MikanSubscriptionSubscriberSourceUrlSchema = type({
+ category: `'${SubscriptionCategoryEnum.MikanSubscriber}'`,
+ mikanSubscriptionToken: 'string>0',
+});
+
+export type MikanSubscriptionSubscriberSourceUrl =
+ typeof MikanSubscriptionSubscriberSourceUrlSchema.infer;
+
+export const MikanSubscriptionSourceUrlSchema =
+ MikanSubscriptionBangumiSourceUrlSchema.or(
+ MikanSubscriptionSeasonSourceUrlSchema
+ ).or(MikanSubscriptionSubscriberSourceUrlSchema);
+
+export type MikanSubscriptionSourceUrl =
+ typeof MikanSubscriptionSourceUrlSchema.infer;
+
+export function isSubscriptionMikanCategory(
+ category: SubscriptionCategoryEnum
+): category is MikanSubscriptionCategoryEnum {
+ return (
+ category === SubscriptionCategoryEnum.MikanBangumi ||
+ category === SubscriptionCategoryEnum.MikanSeason ||
+ category === SubscriptionCategoryEnum.MikanSubscriber
+ );
+}
+
+export function buildMikanSubscriptionSeasonSourceUrl(
+ mikanBaseUrl: string,
+ formParts: MikanSubscriptionSeasonSourceUrl
+): URL {
+ const u = new URL(mikanBaseUrl);
+ u.pathname = MIKAN_SEASON_FLOW_PAGE_PATH;
+ u.searchParams.set(MIKAN_YEAR_QUERY_KEY, formParts.year.toString());
+ u.searchParams.set(MIKAN_SEASON_STR_QUERY_KEY, formParts.seasonStr);
+ return u;
+}
+
+export function buildMikanSubscriptionBangumiSourceUrl(
+ mikanBaseUrl: string,
+ formParts: MikanSubscriptionBangumiSourceUrl
+): URL {
+ const u = new URL(mikanBaseUrl);
+ u.pathname = MIKAN_BANGUMI_RSS_PATH;
+ u.searchParams.set(MIKAN_BANGUMI_ID_QUERY_KEY, formParts.mikanBangumiId);
+ u.searchParams.set(MIKAN_FANSUB_ID_QUERY_KEY, formParts.mikanFansubId);
+ return u;
+}
+
+export function buildMikanSubscriptionSubscriberSourceUrl(
+ mikanBaseUrl: string,
+ formParts: MikanSubscriptionSubscriberSourceUrl
+): URL {
+ const u = new URL(mikanBaseUrl);
+ u.pathname = MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH;
+ u.searchParams.set(
+ MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
+ formParts.mikanSubscriptionToken
+ );
+ return u;
+}
+
+export function buildMikanSubscriptionSourceUrl(
+ mikanBaseUrl: string,
+ formParts: MikanSubscriptionSourceUrl
+): URL {
+ if (formParts.category === SubscriptionCategoryEnum.MikanBangumi) {
+ return buildMikanSubscriptionBangumiSourceUrl(mikanBaseUrl, formParts);
+ }
+ if (formParts.category === SubscriptionCategoryEnum.MikanSeason) {
+ return buildMikanSubscriptionSeasonSourceUrl(mikanBaseUrl, formParts);
+ }
+ if (formParts.category === SubscriptionCategoryEnum.MikanSubscriber) {
+ return buildMikanSubscriptionSubscriberSourceUrl(mikanBaseUrl, formParts);
+ }
+
+ throw new UnimplementedError(
+ // @ts-ignore
+ `source url category = ${formParts.category as any} is not implemented`
+ );
+}
+
+export function extractMikanSubscriptionSeasonSourceUrl(
+ sourceUrl: string
+): MikanSubscriptionSeasonSourceUrl | ArkErrors {
+ const u = new URL(sourceUrl);
+ return MikanSubscriptionSeasonSourceUrlSchema({
+ category: SubscriptionCategoryEnum.MikanSeason,
+ seasonStr: u.searchParams.get(
+ MIKAN_SEASON_STR_QUERY_KEY
+ ) as MikanSeasonEnum,
+ year: Number(u.searchParams.get(MIKAN_YEAR_QUERY_KEY)),
+ });
+}
+
+export function extractMikanSubscriptionBangumiSourceUrl(
+ sourceUrl: string
+): MikanSubscriptionBangumiSourceUrl | ArkErrors {
+ const u = new URL(sourceUrl);
+ return MikanSubscriptionBangumiSourceUrlSchema({
+ category: SubscriptionCategoryEnum.MikanBangumi,
+ mikanBangumiId: u.searchParams.get(MIKAN_BANGUMI_ID_QUERY_KEY),
+ mikanFansubId: u.searchParams.get(MIKAN_FANSUB_ID_QUERY_KEY),
+ });
+}
+
+export function extractMikanSubscriptionSubscriberSourceUrl(
+ sourceUrl: string
+): MikanSubscriptionSubscriberSourceUrl | ArkErrors {
+ const u = new URL(sourceUrl);
+ return MikanSubscriptionSubscriberSourceUrlSchema({
+ category: SubscriptionCategoryEnum.MikanSubscriber,
+ mikanSubscriptionToken: u.searchParams.get(
+ MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY
+ ),
+ });
+}
+
+export function extractMikanSubscriptionSourceUrl(
+ sourceUrl: string
+): MikanSubscriptionSourceUrl | ArkErrors {
+ const u = new URL(sourceUrl);
+ return MikanSubscriptionSourceUrlSchema({
+ category: SubscriptionCategoryEnum.MikanBangumi,
+ mikanBangumiId: u.searchParams.get(MIKAN_BANGUMI_ID_QUERY_KEY),
+ mikanFansubId: u.searchParams.get(MIKAN_FANSUB_ID_QUERY_KEY),
+ });
+}
diff --git a/apps/webui/src/domains/recorder/schema/subscriptions.ts b/apps/webui/src/domains/recorder/schema/subscriptions.ts
index 2b9a479..a989771 100644
--- a/apps/webui/src/domains/recorder/schema/subscriptions.ts
+++ b/apps/webui/src/domains/recorder/schema/subscriptions.ts
@@ -1,5 +1,16 @@
-import type { GetSubscriptionsQuery } from '@/infra/graphql/gql/graphql';
+import { arkValidatorToTypeNarrower } from '@/infra/errors/arktype';
+import {
+ type GetSubscriptionsQuery,
+ SubscriptionCategoryEnum,
+} from '@/infra/graphql/gql/graphql';
import { gql } from '@apollo/client';
+import { type } from 'arktype';
+import {
+ MikanSubscriptionSeasonSourceUrlSchema,
+ extractMikanSubscriptionBangumiSourceUrl,
+ extractMikanSubscriptionSeasonSourceUrl,
+ extractMikanSubscriptionSubscriberSourceUrl,
+} from './mikan';
export const GET_SUBSCRIPTIONS = gql`
query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {
@@ -16,6 +27,7 @@ export const GET_SUBSCRIPTIONS = gql`
category
sourceUrl
enabled
+ credentialId
}
paginationInfo {
total
@@ -25,6 +37,21 @@ export const GET_SUBSCRIPTIONS = gql`
}
`;
+export const INSERT_SUBSCRIPTION = gql`
+ mutation InsertSubscription($data: SubscriptionsInsertInput!) {
+ subscriptionsCreateOne(data: $data) {
+ id
+ createdAt
+ updatedAt
+ displayName
+ category
+ sourceUrl
+ enabled
+ credentialId
+ }
+ }
+`;
+
export type SubscriptionDto =
GetSubscriptionsQuery['subscriptions']['nodes'][number];
@@ -67,6 +94,9 @@ query GetSubscriptionDetail ($id: Int!) {
category
sourceUrl
enabled
+ credential3rd {
+ id
+ }
bangumi {
nodes {
createdAt
@@ -89,3 +119,39 @@ query GetSubscriptionDetail ($id: Int!) {
}
}
`;
+
+export const SubscriptionTypedMikanSeasonSchema =
+ MikanSubscriptionSeasonSourceUrlSchema.and(
+ type({
+ credentialId: 'number>0',
+ })
+ );
+
+export const SubscriptionTypedMikanBangumiSchema = type({
+ category: `'${SubscriptionCategoryEnum.MikanBangumi}'`,
+ sourceUrl: type.string
+ .atLeastLength(1)
+ .narrow(
+ arkValidatorToTypeNarrower(extractMikanSubscriptionBangumiSourceUrl)
+ ),
+});
+
+export const SubscriptionTypedMikanSubscriberSchema = type({
+ category: `'${SubscriptionCategoryEnum.MikanSubscriber}'`,
+ sourceUrl: type.string
+ .atLeastLength(1)
+ .narrow(
+ arkValidatorToTypeNarrower(extractMikanSubscriptionSubscriberSourceUrl)
+ ),
+});
+
+export const SubscriptionTypedSchema = SubscriptionTypedMikanSeasonSchema.or(
+ SubscriptionTypedMikanBangumiSchema
+).or(SubscriptionTypedMikanSubscriberSchema);
+
+export const SubscriptionInsertFormSchema = type({
+ enabled: 'boolean',
+ displayName: 'string>0',
+}).and(SubscriptionTypedSchema);
+
+export type SubscriptionInsertForm = typeof SubscriptionInsertFormSchema.infer;
diff --git a/apps/webui/src/domains/recorder/services/mikan.service.ts b/apps/webui/src/domains/recorder/services/mikan.service.ts
new file mode 100644
index 0000000..653f037
--- /dev/null
+++ b/apps/webui/src/domains/recorder/services/mikan.service.ts
@@ -0,0 +1,6 @@
+import { Injectable } from '@outposts/injection-js';
+
+@Injectable()
+export class MikanService {
+ mikanBaseUrl = 'https://mikanani.me';
+}
diff --git a/apps/webui/src/domains/recorder/services/subscription.service.ts b/apps/webui/src/domains/recorder/services/subscription.service.ts
new file mode 100644
index 0000000..f457908
--- /dev/null
+++ b/apps/webui/src/domains/recorder/services/subscription.service.ts
@@ -0,0 +1,31 @@
+import {
+ SubscriptionCategoryEnum,
+ type SubscriptionsInsertInput,
+} from '@/infra/graphql/gql/graphql';
+import { Injectable, inject } from '@outposts/injection-js';
+import { buildMikanSubscriptionSeasonSourceUrl } from '../schema/mikan';
+import type { SubscriptionInsertForm } from '../schema/subscriptions';
+import { MikanService } from './mikan.service';
+
+@Injectable()
+export class SubscriptionService {
+ private mikan = inject(MikanService);
+
+ transformInsertFormToInput(
+ form: SubscriptionInsertForm
+ ): SubscriptionsInsertInput {
+ let sourceUrl: string;
+ if (form.category === SubscriptionCategoryEnum.MikanSeason) {
+ sourceUrl = buildMikanSubscriptionSeasonSourceUrl(
+ this.mikan.mikanBaseUrl,
+ form
+ ).toString();
+ } else {
+ sourceUrl = form.sourceUrl;
+ }
+ return {
+ ...form,
+ sourceUrl,
+ };
+ }
+}
diff --git a/apps/webui/src/infra/errors/arktype.ts b/apps/webui/src/infra/errors/arktype.ts
new file mode 100644
index 0000000..4b15f8b
--- /dev/null
+++ b/apps/webui/src/infra/errors/arktype.ts
@@ -0,0 +1,15 @@
+import { ArkErrors, type Traversal } from 'arktype';
+
+export function arkValidatorToTypeNarrower<
+ T,
+ V extends (input: T) => unknown | ArkErrors,
+>(validator: V): (input: T, ctx: Traversal) => boolean {
+ return (input, ctx) => {
+ const result = validator(input);
+ if (result instanceof ArkErrors) {
+ ctx.errors.merge(result);
+ return false;
+ }
+ return true;
+ };
+}
diff --git a/apps/webui/src/infra/errors/common.ts b/apps/webui/src/infra/errors/common.ts
index f68e8c2..366d9d0 100644
--- a/apps/webui/src/infra/errors/common.ts
+++ b/apps/webui/src/infra/errors/common.ts
@@ -3,3 +3,9 @@ export class UnreachableError extends Error {
super(`UnreachableError: ${detail}`);
}
}
+
+export class UnimplementedError extends Error {
+ constructor(detail: string) {
+ super(`UnimplementedError: ${detail}`);
+ }
+}
diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts
index 82ae093..5048a9d 100644
--- a/apps/webui/src/infra/graphql/gql/gql.ts
+++ b/apps/webui/src/infra/graphql/gql/gql.ts
@@ -19,11 +19,11 @@ type Documents = {
"\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.UpdateCredential3rdDocument,
"\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": typeof types.DeleteCredential3rdDocument,
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument,
- "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
+ "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
+ "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
- "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
- "\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n": typeof types.CreateSubscriptionDocument,
+ "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
};
const documents: Documents = {
"\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCredential3rdDocument,
@@ -31,11 +31,11 @@ const documents: Documents = {
"\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.UpdateCredential3rdDocument,
"\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": types.DeleteCredential3rdDocument,
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument,
- "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
+ "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
+ "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
- "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
- "\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n": types.CreateSubscriptionDocument,
+ "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
};
/**
@@ -75,7 +75,11 @@ export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\n
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function gql(source: "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
+export function gql(source: "\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
+/**
+ * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function gql(source: "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n"): (typeof documents)["\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -87,11 +91,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filters: Subscr
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
-/**
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function gql(source: "\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n"): (typeof documents)["\n mutation CreateSubscription($input: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $input) {\n id\n displayName\n sourceUrl\n enabled\n category\n }\n }\n"];
+export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"];
export function gql(source: string) {
return (documents as any)[source] ?? {};
diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts
index bf226ed..1078499 100644
--- a/apps/webui/src/infra/graphql/gql/graphql.ts
+++ b/apps/webui/src/infra/graphql/gql/graphql.ts
@@ -1685,7 +1685,14 @@ export type GetSubscriptionsQueryVariables = Exact<{
}>;
-export type GetSubscriptionsQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, createdAt: string, updatedAt: string, displayName: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
+export type GetSubscriptionsQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, createdAt: string, updatedAt: string, displayName: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credentialId?: number | null }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
+
+export type InsertSubscriptionMutationVariables = Exact<{
+ data: SubscriptionsInsertInput;
+}>;
+
+
+export type InsertSubscriptionMutation = { __typename?: 'Mutation', subscriptionsCreateOne: { __typename?: 'SubscriptionsBasic', id: number, createdAt: string, updatedAt: string, displayName: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credentialId?: number | null } };
export type UpdateSubscriptionsMutationVariables = Exact<{
data: SubscriptionsUpdateInput;
@@ -1707,14 +1714,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{
}>;
-export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, rawName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
-
-export type CreateSubscriptionMutationVariables = Exact<{
- input: SubscriptionsInsertInput;
-}>;
-
-
-export type CreateSubscriptionMutation = { __typename?: 'Mutation', subscriptionsCreateOne: { __typename?: 'SubscriptionsBasic', id: number, displayName: string, sourceUrl: string, enabled: boolean, category: SubscriptionCategoryEnum } };
+export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, rawName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } };
export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode;
@@ -1722,8 +1722,8 @@ export const InsertCredential3rdDocument = {"kind":"Document","definitions":[{"k
export const UpdateCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode;
export const DeleteCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode;
export const GetCredential3rdDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rdDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]}}]} as unknown as DocumentNode;
-export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode;
+export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode;
+export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode;
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode;
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode;
-export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode;
-export const CreateSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"category"}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
+export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
diff --git a/apps/webui/src/main.tsx b/apps/webui/src/main.tsx
index 1276132..ce88f98 100644
--- a/apps/webui/src/main.tsx
+++ b/apps/webui/src/main.tsx
@@ -14,9 +14,10 @@ import {
import { Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import './app.css';
+import { provideRecorder } from '@/domains/recorder/context';
+import { provideGraphql } from '@/infra/graphql';
+import { graphqlContextFromInjector } from '@/infra/graphql/context';
import { ApolloProvider } from '@apollo/client';
-import { provideGraphql } from './infra/graphql';
-import { graphqlContextFromInjector } from './infra/graphql/context';
// Create a new router instance
const router = createRouter({
@@ -44,6 +45,7 @@ const injector: Injector = ReflectiveInjector.resolveAndCreate([
...provideAuth(router),
...provideStyles(),
...provideGraphql(),
+ ...provideRecorder(),
]);
setupAuthContext(injector);
diff --git a/apps/webui/src/presentation/routeTree.gen.ts b/apps/webui/src/presentation/routeTree.gen.ts
index 6964d4f..81339b8 100644
--- a/apps/webui/src/presentation/routeTree.gen.ts
+++ b/apps/webui/src/presentation/routeTree.gen.ts
@@ -32,8 +32,8 @@ import { Route as AppCredential3rdCreateImport } from './routes/_app/credential3
import { Route as AppBangumiManageImport } from './routes/_app/bangumi/manage'
import { Route as AppExploreFeedImport } from './routes/_app/_explore/feed'
import { Route as AppExploreExploreImport } from './routes/_app/_explore/explore'
-import { Route as AppSubscriptionsEditSubscriptionIdImport } from './routes/_app/subscriptions/edit.$subscriptionId'
-import { Route as AppSubscriptionsDetailSubscriptionIdImport } from './routes/_app/subscriptions/detail.$subscriptionId'
+import { Route as AppSubscriptionsEditIdImport } from './routes/_app/subscriptions/edit.$id'
+import { Route as AppSubscriptionsDetailIdImport } from './routes/_app/subscriptions/detail.$id'
import { Route as AppCredential3rdEditIdImport } from './routes/_app/credential3rd/edit.$id'
import { Route as AppCredential3rdDetailIdImport } from './routes/_app/credential3rd/detail.$id'
@@ -166,19 +166,17 @@ const AppExploreExploreRoute = AppExploreExploreImport.update({
getParentRoute: () => AppRouteRoute,
} as any)
-const AppSubscriptionsEditSubscriptionIdRoute =
- AppSubscriptionsEditSubscriptionIdImport.update({
- id: '/edit/$subscriptionId',
- path: '/edit/$subscriptionId',
- getParentRoute: () => AppSubscriptionsRouteRoute,
- } as any)
+const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdImport.update({
+ id: '/edit/$id',
+ path: '/edit/$id',
+ getParentRoute: () => AppSubscriptionsRouteRoute,
+} as any)
-const AppSubscriptionsDetailSubscriptionIdRoute =
- AppSubscriptionsDetailSubscriptionIdImport.update({
- id: '/detail/$subscriptionId',
- path: '/detail/$subscriptionId',
- getParentRoute: () => AppSubscriptionsRouteRoute,
- } as any)
+const AppSubscriptionsDetailIdRoute = AppSubscriptionsDetailIdImport.update({
+ id: '/detail/$id',
+ path: '/detail/$id',
+ getParentRoute: () => AppSubscriptionsRouteRoute,
+} as any)
const AppCredential3rdEditIdRoute = AppCredential3rdEditIdImport.update({
id: '/edit/$id',
@@ -357,18 +355,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppCredential3rdEditIdImport
parentRoute: typeof AppCredential3rdRouteImport
}
- '/_app/subscriptions/detail/$subscriptionId': {
- id: '/_app/subscriptions/detail/$subscriptionId'
- path: '/detail/$subscriptionId'
- fullPath: '/subscriptions/detail/$subscriptionId'
- preLoaderRoute: typeof AppSubscriptionsDetailSubscriptionIdImport
+ '/_app/subscriptions/detail/$id': {
+ id: '/_app/subscriptions/detail/$id'
+ path: '/detail/$id'
+ fullPath: '/subscriptions/detail/$id'
+ preLoaderRoute: typeof AppSubscriptionsDetailIdImport
parentRoute: typeof AppSubscriptionsRouteImport
}
- '/_app/subscriptions/edit/$subscriptionId': {
- id: '/_app/subscriptions/edit/$subscriptionId'
- path: '/edit/$subscriptionId'
- fullPath: '/subscriptions/edit/$subscriptionId'
- preLoaderRoute: typeof AppSubscriptionsEditSubscriptionIdImport
+ '/_app/subscriptions/edit/$id': {
+ id: '/_app/subscriptions/edit/$id'
+ path: '/edit/$id'
+ fullPath: '/subscriptions/edit/$id'
+ preLoaderRoute: typeof AppSubscriptionsEditIdImport
parentRoute: typeof AppSubscriptionsRouteImport
}
}
@@ -432,17 +430,15 @@ const AppSettingsRouteRouteWithChildren =
interface AppSubscriptionsRouteRouteChildren {
AppSubscriptionsCreateRoute: typeof AppSubscriptionsCreateRoute
AppSubscriptionsManageRoute: typeof AppSubscriptionsManageRoute
- AppSubscriptionsDetailSubscriptionIdRoute: typeof AppSubscriptionsDetailSubscriptionIdRoute
- AppSubscriptionsEditSubscriptionIdRoute: typeof AppSubscriptionsEditSubscriptionIdRoute
+ AppSubscriptionsDetailIdRoute: typeof AppSubscriptionsDetailIdRoute
+ AppSubscriptionsEditIdRoute: typeof AppSubscriptionsEditIdRoute
}
const AppSubscriptionsRouteRouteChildren: AppSubscriptionsRouteRouteChildren = {
AppSubscriptionsCreateRoute: AppSubscriptionsCreateRoute,
AppSubscriptionsManageRoute: AppSubscriptionsManageRoute,
- AppSubscriptionsDetailSubscriptionIdRoute:
- AppSubscriptionsDetailSubscriptionIdRoute,
- AppSubscriptionsEditSubscriptionIdRoute:
- AppSubscriptionsEditSubscriptionIdRoute,
+ AppSubscriptionsDetailIdRoute: AppSubscriptionsDetailIdRoute,
+ AppSubscriptionsEditIdRoute: AppSubscriptionsEditIdRoute,
}
const AppSubscriptionsRouteRouteWithChildren =
@@ -498,8 +494,8 @@ export interface FileRoutesByFullPath {
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
- '/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
- '/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
+ '/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
+ '/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
}
export interface FileRoutesByTo {
@@ -526,8 +522,8 @@ export interface FileRoutesByTo {
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
- '/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
- '/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
+ '/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
+ '/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
}
export interface FileRoutesById {
@@ -555,8 +551,8 @@ export interface FileRoutesById {
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/_app/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
- '/_app/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
- '/_app/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
+ '/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
+ '/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
}
export interface FileRouteTypes {
@@ -585,8 +581,8 @@ export interface FileRouteTypes {
| '/auth/oidc/callback'
| '/credential3rd/detail/$id'
| '/credential3rd/edit/$id'
- | '/subscriptions/detail/$subscriptionId'
- | '/subscriptions/edit/$subscriptionId'
+ | '/subscriptions/detail/$id'
+ | '/subscriptions/edit/$id'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -612,8 +608,8 @@ export interface FileRouteTypes {
| '/auth/oidc/callback'
| '/credential3rd/detail/$id'
| '/credential3rd/edit/$id'
- | '/subscriptions/detail/$subscriptionId'
- | '/subscriptions/edit/$subscriptionId'
+ | '/subscriptions/detail/$id'
+ | '/subscriptions/edit/$id'
id:
| '__root__'
| '/'
@@ -639,8 +635,8 @@ export interface FileRouteTypes {
| '/auth/oidc/callback'
| '/_app/credential3rd/detail/$id'
| '/_app/credential3rd/edit/$id'
- | '/_app/subscriptions/detail/$subscriptionId'
- | '/_app/subscriptions/edit/$subscriptionId'
+ | '/_app/subscriptions/detail/$id'
+ | '/_app/subscriptions/edit/$id'
fileRoutesById: FileRoutesById
}
@@ -741,8 +737,8 @@ export const routeTree = rootRoute
"children": [
"/_app/subscriptions/create",
"/_app/subscriptions/manage",
- "/_app/subscriptions/detail/$subscriptionId",
- "/_app/subscriptions/edit/$subscriptionId"
+ "/_app/subscriptions/detail/$id",
+ "/_app/subscriptions/edit/$id"
]
},
"/auth/sign-in": {
@@ -798,12 +794,12 @@ export const routeTree = rootRoute
"filePath": "_app/credential3rd/edit.$id.tsx",
"parent": "/_app/credential3rd"
},
- "/_app/subscriptions/detail/$subscriptionId": {
- "filePath": "_app/subscriptions/detail.$subscriptionId.tsx",
+ "/_app/subscriptions/detail/$id": {
+ "filePath": "_app/subscriptions/detail.$id.tsx",
"parent": "/_app/subscriptions"
},
- "/_app/subscriptions/edit/$subscriptionId": {
- "filePath": "_app/subscriptions/edit.$subscriptionId.tsx",
+ "/_app/subscriptions/edit/$id": {
+ "filePath": "_app/subscriptions/edit.$id.tsx",
"parent": "/_app/subscriptions"
}
}
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx
index b0c635d..9c112ad 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx
@@ -80,13 +80,11 @@ function CredentialCreateRouteComponent() {
...form.value,
userAgent: form.value.userAgent || platformService.userAgent,
};
- if (form.value.credentialType === Credential3rdTypeEnum.Mikan) {
- await insertCredential3rd({
- variables: {
- data: value,
- },
- });
- }
+ await insertCredential3rd({
+ variables: {
+ data: value,
+ },
+ });
},
});
@@ -118,16 +116,7 @@ function CredentialCreateRouteComponent() {
}}
className="space-y-6"
>
- {
- if (!value) {
- return 'Please select the credential type';
- }
- },
- }}
- >
+
{(field) => (
@@ -147,7 +136,11 @@ function CredentialCreateRouteComponent() {
{field.state.meta.errors && (
-
+
)}
)}
@@ -166,7 +159,11 @@ function CredentialCreateRouteComponent() {
autoComplete="off"
/>
{field.state.meta.errors && (
-
+
)}
)}
@@ -188,7 +185,11 @@ function CredentialCreateRouteComponent() {
autoComplete="off"
/>
{field.state.meta.errors && (
-
+
)}
)}
@@ -215,7 +216,11 @@ function CredentialCreateRouteComponent() {
Current default user agent: {platformService.userAgent}
{field.state.meta.errors && (
-
+
)}
)}
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx
index fe0d832..3571ced 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx
@@ -91,7 +91,7 @@ function Credential3rdDetailRouteComponent() {
Credential detail
- View and manage credential #{credential.id}
+ View credential #{credential.id}
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx
index 4f89961..6f229b0 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx
@@ -117,9 +117,9 @@ function FormView({
-
Credential detail
+
Credential edit
- View and manage credential #{credential.id}
+ Edit credential #{credential.id}
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx
index a8d0601..bec79ef 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx
@@ -22,6 +22,7 @@ import type { GetCredential3rdQuery } from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
import { useEvent } from '@/presentation/hooks/use-event';
+import { cn } from '@/presentation/utils';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
@@ -332,14 +333,24 @@ function CredentialManageRouteComponent() {
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
- )}
-
- ))}
+ {row.getVisibleCells().map((cell) => {
+ const isPinned = cell.column.getIsPinned();
+ return (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ );
+ })}
))
) : (
diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/-credential3rd-select.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/-credential3rd-select.tsx
new file mode 100644
index 0000000..c520d67
--- /dev/null
+++ b/apps/webui/src/presentation/routes/_app/subscriptions/-credential3rd-select.tsx
@@ -0,0 +1,93 @@
+import { Button } from '@/components/ui/button';
+import { SelectContent, SelectItem } from '@/components/ui/select';
+import { GET_CREDENTIAL_3RD } from '@/domains/recorder/schema/credential3rd';
+import {
+ type Credential3rdTypeEnum,
+ type GetCredential3rdQuery,
+ type GetCredential3rdQueryVariables,
+ OrderByEnum,
+} from '@/infra/graphql/gql/graphql';
+import { useQuery } from '@apollo/client';
+import { AlertCircle, Loader2, RefreshCw } from 'lucide-react';
+import type { ComponentProps } from 'react';
+
+export interface Credential3rdSelectContentProps
+ extends ComponentProps {
+ credentialType: Credential3rdTypeEnum;
+}
+
+export function Credential3rdSelectContent({
+ credentialType,
+ ...props
+}: Credential3rdSelectContentProps) {
+ const { data, loading, error, refetch } = useQuery<
+ GetCredential3rdQuery,
+ GetCredential3rdQueryVariables
+ >(GET_CREDENTIAL_3RD, {
+ fetchPolicy: 'cache-and-network',
+ nextFetchPolicy: 'cache-and-network',
+ variables: {
+ filters: {
+ credentialType: {
+ eq: credentialType,
+ },
+ },
+ orderBy: {
+ createdAt: OrderByEnum.Desc,
+ },
+ pagination: {
+ page: {
+ page: 0,
+ limit: 100,
+ },
+ },
+ },
+ });
+
+ const credentials = data?.credential3rd?.nodes ?? [];
+
+ return (
+
+ {loading && (
+
+
+ Loading...
+
+ )}
+
+ {error && (
+
+
+
+
Failed to load credentials
+
+
+
+ )}
+
+ {!loading &&
+ !error &&
+ (credentials.length === 0 ? (
+
+
+ No credentials found
+
+
+ ) : (
+ credentials.map((credential) => (
+
+ {credential.username}
+
+ ))
+ ))}
+
+ );
+}
diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/create.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/create.tsx
index 5adb42e..df558d0 100644
--- a/apps/webui/src/presentation/routes/_app/subscriptions/create.tsx
+++ b/apps/webui/src/presentation/routes/_app/subscriptions/create.tsx
@@ -1,23 +1,14 @@
-import { useAuth } from '@/app/auth/hooks';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
- CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@/components/ui/form';
+import { FormFieldErrors } from '@/components/ui/form-field-errors';
import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@@ -26,13 +17,28 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
+import { useAppForm } from '@/components/ui/tanstack-form';
+import { MikanSeasonEnum } from '@/domains/recorder/schema/mikan';
+import {
+ INSERT_SUBSCRIPTION,
+ type SubscriptionInsertForm,
+ SubscriptionInsertFormSchema,
+} from '@/domains/recorder/schema/subscriptions';
+import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
+import { useInject } from '@/infra/di/inject';
+import {
+ Credential3rdTypeEnum,
+ type InsertSubscriptionMutation,
+ type InsertSubscriptionMutationVariables,
+ SubscriptionCategoryEnum,
+} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
-import { gql, useMutation } from '@apollo/client';
+import { useMutation } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { useNavigate } from '@tanstack/react-router';
-import { useState } from 'react';
-import { useForm } from 'react-hook-form';
+import { Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
+import { Credential3rdSelectContent } from './-credential3rd-select';
export const Route = createFileRoute('/_app/subscriptions/create')({
component: SubscriptionCreateRouteComponent,
@@ -41,194 +47,312 @@ export const Route = createFileRoute('/_app/subscriptions/create')({
} satisfies RouteStateDataOption,
});
-type SubscriptionFormValues = {
- displayName: string;
- sourceUrl: string;
- category: string;
- enabled: boolean;
-};
-
-const CREATE_SUBSCRIPTION_MUTATION = gql`
- mutation CreateSubscription($input: SubscriptionsInsertInput!) {
- subscriptionsCreateOne(data: $input) {
- id
- displayName
- sourceUrl
- enabled
- category
- }
- }
-`;
-
function SubscriptionCreateRouteComponent() {
- const { authData } = useAuth();
- console.log(JSON.stringify(authData, null, 2));
- const [isSubmitting, setIsSubmitting] = useState(false);
const navigate = useNavigate();
- const form = useForm({
- defaultValues: {
- displayName: '',
- sourceUrl: '',
- category: 'mikan',
- enabled: true,
+ const subscriptionService = useInject(SubscriptionService);
+
+ const [insertSubscription, { loading }] = useMutation<
+ InsertSubscriptionMutation['subscriptionsCreateOne'],
+ InsertSubscriptionMutationVariables
+ >(INSERT_SUBSCRIPTION, {
+ onCompleted(data) {
+ toast.success('Subscription created');
+ navigate({
+ to: '/subscriptions/detail/$id',
+ params: { id: `${data.id}` },
+ });
+ },
+ onError(error) {
+ toast.error('Failed to create subscription', {
+ description: error.message,
+ });
},
});
- const [createSubscription] = useMutation(CREATE_SUBSCRIPTION_MUTATION);
-
- const onSubmit = async (data: SubscriptionFormValues) => {
- try {
- setIsSubmitting(true);
- const response = await createSubscription({
+ const form = useAppForm({
+ defaultValues: {
+ displayName: '',
+ category: undefined,
+ enabled: true,
+ sourceUrl: '',
+ credentialId: '',
+ year: undefined,
+ seasonStr: '',
+ } as unknown as SubscriptionInsertForm,
+ validators: {
+ onBlur: SubscriptionInsertFormSchema,
+ onSubmit: SubscriptionInsertFormSchema,
+ },
+ onSubmit: async (form) => {
+ const input = subscriptionService.transformInsertFormToInput(form.value);
+ await insertSubscription({
variables: {
- input: {
- category: data.category,
- displayName: data.displayName,
- sourceUrl: data.sourceUrl,
- enabled: data.enabled,
- },
+ data: input,
},
});
-
- if (response.errors) {
- throw new Error(
- response.errors[0]?.message || 'Failed to create subscription'
- );
- }
-
- toast.success('Subscription created successfully');
- navigate({ to: '/subscriptions/manage' });
- } catch (error) {
- console.error('Failed to create subscription:', error);
- toast.error(
- `Subscription creation failed: ${
- error instanceof Error ? error.message : 'Unknown error'
- }`
- );
- } finally {
- setIsSubmitting(false);
- }
- };
+ },
+ });
return (
-
-
- Create Bangumi Subscription
- Add a new bangumi subscription source
-
-
-