Compare commits

..

54 Commits

Author SHA1 Message Date
jxxghp
121cb7e442 v1.7.3 2024-03-16 18:28:32 +08:00
jxxghp
44163f0fb2 fix bug 2024-03-16 17:20:49 +08:00
jxxghp
d43865fcad fix message ui 2024-03-16 17:16:10 +08:00
jxxghp
fed92f3853 fix message ui 2024-03-16 16:47:41 +08:00
jxxghp
823d2a816e fix 2024-03-16 08:40:44 +08:00
jxxghp
046c21edf6 add message view 2024-03-15 18:15:31 +08:00
jxxghp
8236d80b42 feat:优化插件升级使用体验 2024-03-12 21:31:56 +08:00
jxxghp
90e7eb1c79 Merge pull request #86 from WangEdward/main 2024-03-11 16:32:30 +08:00
WangEdward
ef09868af1 fix: display search_imdbid status 2024-03-11 16:30:24 +08:00
jxxghp
028981e3ae 更新 package.json 2024-03-10 21:07:17 +08:00
jxxghp
e8a6274cf6 Merge pull request #85 from honue/main 2024-03-10 17:16:54 +08:00
honue
ffd0265526 设置每周一为第一天,当天背景突显 2024-03-10 16:49:40 +08:00
jxxghp
13d7344bc0 fix bug 2024-03-09 20:53:37 +08:00
jxxghp
2ad36f92c5 feat:下载器多选 2024-03-09 18:52:48 +08:00
jxxghp
36b02f4423 release 2024-03-09 17:35:45 +08:00
jxxghp
01df990aa8 fix #1638 2024-03-09 17:05:44 +08:00
jxxghp
49b71fcf5d fix #1640 2024-03-09 16:49:48 +08:00
jxxghp
9e43d77ac4 feat:新增官种优先级规则 2024-03-09 09:15:03 +08:00
jxxghp
3ab9af720b Merge pull request #84 from WangEdward/main 2024-03-08 22:11:13 +08:00
WangEdward
abae304f87 chore: add half-increments for v-rating 2024-03-08 21:23:25 +08:00
jxxghp
659d8bff66 fix 下载及订阅用户匹配 2024-03-08 15:27:51 +08:00
jxxghp
1786e10101 fix:修改编译策略,避免一直loading 2024-03-08 14:18:30 +08:00
jxxghp
bda7f929e7 try fix loading 2024-03-08 13:31:38 +08:00
jxxghp
c309f80a94 更新 ModuleTestView.vue 2024-03-06 22:05:12 +08:00
jxxghp
97c987c561 fix ui 2024-03-06 21:52:20 +08:00
jxxghp
48949104e0 feat:目录检测 2024-03-06 21:41:06 +08:00
jxxghp
a38cc4fe34 add min_seeders 2024-03-06 20:15:23 +08:00
jxxghp
495dfbcb28 feat:add VoceChat 2024-03-06 15:55:01 +08:00
jxxghp
6e4dbd912b feat:健康检查 2024-03-06 13:27:17 +08:00
jxxghp
82904d956d thetvdb network test 2024-03-06 11:16:12 +08:00
jxxghp
ec7118b376 fix #82 后台登录成功但前端报错 2024-03-05 20:21:05 +08:00
jxxghp
058b32a263 release 2024-03-05 17:27:27 +08:00
jxxghp
e7b960838e Merge pull request #82 from honue/main 2024-03-05 15:24:46 +08:00
honue
14e776a287 登录后返回原始页面功能 2024-03-05 15:20:27 +08:00
jxxghp
73f11b920f Merge pull request #81 from WangEdward/main
feat: add search_imdbid
2024-03-05 12:57:44 +08:00
jxxghp
5c93040a8e fanart nettest 2024-03-05 12:57:02 +08:00
WangEdward
a517769e8a feat: add search_imdbid 2024-03-04 21:36:06 +08:00
jxxghp
4bb59a9f05 fix 2024-03-03 09:02:41 +08:00
jxxghp
b37879d2d4 release 2024-03-03 08:48:04 +08:00
jxxghp
05defc39d7 rollback VTooltip 2024-03-03 08:47:20 +08:00
jxxghp
18bfad07d2 fix 2024-03-02 09:58:42 +08:00
jxxghp
b83591255d Merge pull request #80 from maicss/main 2024-03-02 09:14:56 +08:00
maicss
804350bc81 feat: search box add border for contrast 2024-03-01 23:33:50 +08:00
maicss
46e1cae0bb feat: add session valid chack 2024-03-01 23:33:50 +08:00
maicss
81062d4580 fix: enhance some interactive experiences 2024-03-01 23:33:50 +08:00
maicss
55481db2ee Merge branch 'jxxghp:main' into main 2024-03-01 20:16:10 +08:00
jxxghp
ecdd12f5a9 fix 2024-02-29 20:21:30 +08:00
jxxghp
ef92cdc183 fix torrent type 2024-02-29 16:15:28 +08:00
jxxghp
08f4a6cf2c Merge pull request #78 from maicss/maicss 2024-02-29 11:16:48 +08:00
maicss
38889acb4e fix: toast message not visible
When install plugin failed, it will yield an error toast. But attribute `z-index` of toast is `1090`, while `v-dialog` is `2400`
2024-02-29 09:51:20 +08:00
maicss
c0517cd29a fix: toast message not visible
When install plugin failed, `z-index` of toast is `1090` while `v-dialog` is `2400`.
2024-02-29 09:48:23 +08:00
jxxghp
084449ccf3 rebuild 2024-02-28 18:54:35 +08:00
jxxghp
0e8203ae03 fix app 2024-02-27 19:25:12 +08:00
jxxghp
236440be52 fix form 2024-02-27 15:23:07 +08:00
37 changed files with 1073 additions and 282 deletions

View File

@@ -1,214 +1,162 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
<title>MoviePilot</title>
<meta name="Robots" content="noindex,nofollow,noarchive" />
<meta name="referrer" content="origin" />
<link rel="icon" type="image/png" href="/logo.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
<meta name="description" content="MoviePilot" />
<meta name="format-detection" content="telephone=no" />
<meta name="referrer" content="never" />
<meta name="msapplication-TileColor" content="#7D34FD" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<link rel="stylesheet" type="text/css" href="/loader.css" />
</head>
<body> <head>
<div id="loading-bg"> <meta http-equiv="pragma" content="no-cache">
<div class="loading-logo"> <meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<!-- Logo --> <meta http-equiv="expires" content="0">
<svg <meta charset="UTF-8" />
width="100px" <link rel="icon" href="/favicon.ico" />
height="100px" <meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
viewBox="0 0 192 192" <title>MoviePilot</title>
version="1.1" <meta name="Robots" content="noindex,nofollow,noarchive" />
xmlns="http://www.w3.org/2000/svg" <meta name="referrer" content="origin" />
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2" <link rel="icon" type="image/png" href="/logo.png" />
> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<g transform="matrix(1,0,0,1,-2606,-236)"> <link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<rect x="0" y="0" width="192" height="192" style="fill: none" /> <link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
<meta name="description" content="MoviePilot" />
<meta name="format-detection" content="telephone=no" />
<meta name="referrer" content="never" />
<meta name="msapplication-TileColor" content="#7D34FD" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<link rel="stylesheet" type="text/css" href="/loader.css" />
</head>
<body>
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<svg width="100px" height="100px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
<g transform="matrix(1,0,0,1,-2606,-236)">
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
<rect x="0" y="0" width="192" height="192" style="fill: none" />
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
<path
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
style="fill: url(#_Linear1)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
style="fill: url(#_Linear2)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
style="fill: rgb(141, 81, 249)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
style="fill: url(#_Linear3)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
style="fill: rgb(165, 118, 255)" />
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
style="fill: url(#_Linear4)" />
</g>
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
<path
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
style="fill: rgb(141, 81, 249)" />
</g>
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
<path
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
style="fill: rgb(104, 0, 197)" />
<clipPath id="_clip5">
<path <path
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z" d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
style="fill: url(#_Linear1)" </clipPath>
/> <g clip-path="url(#_clip5)">
</g> <g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
style="fill: url(#_Linear2)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
style="fill: rgb(141, 81, 249)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
style="fill: url(#_Linear3)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
style="fill: rgb(165, 118, 255)"
/>
</g>
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
<path
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
style="fill: url(#_Linear4)"
/>
</g>
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
<path
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
style="fill: rgb(141, 81, 249)"
/>
</g>
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
<path
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
style="fill: rgb(104, 0, 197)"
/>
<clipPath id="_clip5">
<path <path
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
/> style="fill: url(#_Linear6)" />
</clipPath> </g>
<g clip-path="url(#_clip5)"> <g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)"> <path
<path d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z" style="fill: rgb(141, 81, 249)" />
style="fill: url(#_Linear6)" </g>
/> <g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
</g> <path
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)"> d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
<path style="fill: url(#_Radial7)" />
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
style="fill: rgb(141, 81, 249)"
/>
</g>
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
<path
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
style="fill: url(#_Radial7)"
/>
</g>
</g> </g>
</g> </g>
</g> </g>
</g> </g>
<defs> </g>
<linearGradient <defs>
id="_Linear1" <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
x1="0" gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
y1="0" <stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
x2="1" <stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
y2="0" </linearGradient>
gradientUnits="userSpaceOnUse" <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)" gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
> <stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" /> <stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" /> </linearGradient>
</linearGradient> <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
<linearGradient gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
id="_Linear2" <stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
x1="0" <stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
y1="0" </linearGradient>
x2="1" <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
y2="0" gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
gradientUnits="userSpaceOnUse" <stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)" <stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
> </linearGradient>
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" /> <linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" /> gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
</linearGradient> <stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
<linearGradient <stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
id="_Linear3" <stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
x1="0" </linearGradient>
y1="0" <radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
x2="1" gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
y2="0" <stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
gradientUnits="userSpaceOnUse" <stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)" </radialGradient>
> </defs>
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" /> </svg>
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
</linearGradient>
<linearGradient
id="_Linear4"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
>
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
</linearGradient>
<linearGradient
id="_Linear6"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
>
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
</linearGradient>
<radialGradient
id="_Radial7"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
>
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
</radialGradient>
</defs>
</svg>
</div>
<div class="loading">
<div class="effect-1 effects"></div>
<div class="effect-2 effects"></div>
<div class="effect-3 effects"></div>
</div>
</div> </div>
<div id="app"></div> <div class="loading">
<script type="module" src="/src/main.ts"></script> <div class="effect-1 effects"></div>
<script> <div class="effect-2 effects"></div>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF' <div class="effect-3 effects"></div>
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD' </div>
</div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor) if (loaderColor)
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor) if (primaryColor)
</script> document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</body> </script>
</html> </body>
</html>

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.6.8", "version": "1.7.3",
"private": true, "private": true,
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {

View File

@@ -121,11 +121,7 @@ function themeTransition() {
<template> <template>
<IconBtn @click="changeTheme"> <IconBtn @click="changeTheme">
<VTooltip text="切换主题"> <VIcon :icon="props.themes[currentThemeIndex].icon" />
<template #activator="{ props: _props }">
<VIcon v-bind="_props" :icon="props.themes[currentThemeIndex].icon" />
</template>
</VTooltip>
</IconBtn> </IconBtn>
</template> </template>

View File

@@ -147,3 +147,23 @@ export function formatEp(nums: number[]): string {
return formattedRanges.join('、') return formattedRanges.join('、')
} }
// 将yyyy-mm-dd hh:mm:ss转换为时间差1小时前1天前
export function formatDateDifference(dateString: string): string {
const date = new Date(dateString)
const currentDate = new Date()
const timeDifference = currentDate.getTime() - date.getTime()
const secondsDifference = Math.floor(timeDifference / 1000)
const minutesDifference = Math.floor(secondsDifference / 60)
const hoursDifference = Math.floor(minutesDifference / 60)
const daysDifference = Math.floor(hoursDifference / 24)
if (daysDifference > 0)
return `${daysDifference}天前`
else if (hoursDifference > 0)
return `${hoursDifference}小时前`
else if (minutesDifference > 0)
return `${minutesDifference}分钟前`
else
return '刚刚'
}

View File

@@ -1,8 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import store from './store' import store from './store'
// 提示框
const $toast = useToast()
// 设置主题
function setTheme() { function setTheme() {
const { global: globalTheme } = useTheme() const { global: globalTheme } = useTheme()
let theme = localStorage.getItem('theme') || 'light' let theme = localStorage.getItem('theme') || 'light'
@@ -10,11 +15,6 @@ function setTheme() {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
globalTheme.name.value = theme globalTheme.name.value = theme
} }
// 第一时间应用主题
setTheme()
// 提示框
const $toast = useToast()
// SSE持续接收消息 // SSE持续接收消息
function startSSEMessager() { function startSSEMessager() {
@@ -38,6 +38,7 @@ function startSSEMessager() {
// 页面加载时,加载当前用户数据 // 页面加载时,加载当前用户数据
onBeforeMount(async () => { onBeforeMount(async () => {
setTheme()
startSSEMessager() startSSEMessager()
}) })
</script> </script>

View File

@@ -80,6 +80,9 @@ export interface Subscribe {
// 是否洗版数字或者boolean // 是否洗版数字或者boolean
best_version: any best_version: any
// 使用 imdbid 搜索
search_imdbid?: any
// 当前优先级 // 当前优先级
current_priority: number current_priority: number
@@ -510,8 +513,11 @@ export interface DownloadingInfo {
// 媒体信息 // 媒体信息
media: { [key: string]: any } media: { [key: string]: any }
// 下载用户 // 下载用户ID
userid?: string userid?: string
// 下载用户名称
username?: string
} }
// 缺失剧集信息 // 缺失剧集信息
@@ -658,6 +664,9 @@ export interface TorrentInfo {
// 剩余免费时间 // 剩余免费时间
freedate_diff: string freedate_diff: string
// 种子类型
category: string
} }
// 识别元数据 // 识别元数据
@@ -793,18 +802,34 @@ export interface Context {
// 用户信息 // 用户信息
export interface User { export interface User {
// 用户ID
id: number id: number
// 用户名称
name: string name: string
// 用户密码
password: string password: string
// 用户邮箱
email: string email: string
// 是否激活
is_active: boolean is_active: boolean
// 是否管理员
is_superuser: boolean is_superuser: boolean
// 头像
avatar: string avatar: string
} }
// 存储空间 // 存储空间
export interface Storage { export interface Storage {
// 总空间
total_storage: number total_storage: number
// 已使用空间
used_storage: number used_storage: number
} }
@@ -898,6 +923,7 @@ export interface NotificationSwitch {
telegram: boolean telegram: boolean
slack: boolean slack: boolean
synologychat: boolean synologychat: boolean
vocechat: boolean
} }
// 环境设置 // 环境设置
@@ -908,45 +934,132 @@ export interface Setting {
// 文件浏览接口 // 文件浏览接口
export interface EndPoints { export interface EndPoints {
// 文件列表
list: any list: any
// 创建目录
mkdir: any mkdir: any
// 删除文件
delete: any delete: any
// 下载文件
download: any download: any
// 图片预览
image: any image: any
// 重命名
rename: any rename: any
} }
// 文件浏览项目 // 文件浏览项目
export interface FileItem { export interface FileItem {
// 类型
type: string type: string
// 文件名
name: string name: string
// 文件名不含扩展名
basename: string basename: string
// 文件路径
path: string path: string
// 文件扩展名
extension: string extension: string
// 文件大小
size: number size: number
// 文件子元素
children: FileItem[] children: FileItem[]
// 文件创建时间
modify_time: number modify_time: number
} }
// 媒体服务器播放条目 // 媒体服务器播放条目
export interface MediaServerPlayItem { export interface MediaServerPlayItem {
// ID
id?: string | number id?: string | number
// 标题
title: string title: string
// 副标题
subtitle?: string subtitle?: string
// 类型
type?: string type?: string
// 海报
image?: string image?: string
// 链接
link?: string link?: string
// 播放百分比
percent?: number percent?: number
} }
// 媒体服务器媒体库 // 媒体服务器媒体库
export interface MediaServerLibrary { export interface MediaServerLibrary {
// 服务器名称
server: string server: string
// ID
id?: string | number id?: string | number
// 名称
name: string name: string
// 路径
path?: string path?: string
// 类型
type?: string type?: string
// 图片
image?: string image?: string
// 图片列表
image_list?: string[] image_list?: string[]
// 链接
link?: string link?: string
} }
// 消息通知
export interface Message {
// 消息类型
mtype?: string
// 消息标题
title?: string
// 消息内容
text?: string
// 消息链接
link?: string
// 消息图片
image?: string
// 消息时间
date?: string
// 登记时间
reg_time?: string
// 用户ID
userid?: string
// 消息方向0-接收1-发送
action?: number
// JSON
note?: string
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -25,8 +25,8 @@ const isDownloading = ref(props.info?.state === 'downloading')
// 监听props.info?.state的变化 // 监听props.info?.state的变化
watch(() => props.info?.state, (newValue) => { watch(() => props.info?.state, (newValue) => {
isDownloading.value = newValue === 'downloading'; isDownloading.value = newValue === 'downloading'
}); })
// 图片是否加载完成 // 图片是否加载完成
const imageLoaded = ref(false) const imageLoaded = ref(false)

View File

@@ -36,6 +36,7 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' }, { title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' }, { title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' }, { title: '国语配音', value: ' CNVOI ' },
{ title: '官种', value: ' GZ ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' }, { title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' }, { title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' }, { title: '排除: 粤语配音', value: ' !HKVOI ' },

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { Message } from '@/api/types'
import { formatDateDifference } from '@core/utils/formatters'
// 输入参数
const props = defineProps({
message: Object as PropType<Message>,
width: String,
height: String,
})
// 图片是否加载完成
const isImageLoaded = ref(false)
// 图片是否加载失败
const imageLoadError = ref(false)
// 图片加载完成
async function imageLoaded() {
isImageLoaded.value = true
}
// 链接打开新窗口
function openLink() {
if (props.message?.link)
window.open(props.message.link, '_blank')
}
// 将note转换为json
function noteToJson() {
if (props.message?.note) {
try {
return JSON.parse(props.message.note)
}
catch (error) {
console.error(error)
}
}
return {}
}
// 将\n转换为html属性的换行符
function replaceNewLine(value: string) {
if (!value)
return ''
return value.replace(/\n/g, '<br/>')
}
</script>
<template>
<VCard
:width="props.width"
:height="props.height"
variant="tonal"
@click="openLink"
>
<div
v-if="props.message?.image"
class="relative text-center card-cover-blurred"
>
<VImg
:src="props.message?.image"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</div>
<VCardTitle v-if="props.message?.title" class="whitespace-break-spaces">
{{ props.message?.title }}
</VCardTitle>
<VAlert
v-if="props.message?.text && props.message?.action === 0"
variant="tonal"
type="success"
>
<template #prepend />
{{ props.message?.text }}
</VAlert>
<VCardText
v-if="props.message?.text && props.message?.action === 1"
v-html="replaceNewLine(props.message?.text)"
/>
<VCardText v-if="props.message?.note">
<VList>
<VListItem
v-for="(value, key) in noteToJson()"
:key="key"
two-line
>
<VListItemTitle v-if="value.title_year" class="font-bold">
{{ key + 1 }}. {{ value.title_year }}
</VListItemTitle>
<VListItemTitle v-if="value.enclosure" class="font-bold whitespace-break-spaces">
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} {{ value.seeders }}
</VListItemTitle>
<VListItemSubtitle v-if="value.type">
类型{{ value.type }} 评分{{ value.vote_average }}
</VListItemSubtitle>
<VListItemSubtitle v-if="value.enclosure" class="whitespace-break-spaces">
{{ value.description }}
</VListItemSubtitle>
</VListItem>
</VList>
</VCardText>
<div class="text-end">
<span v-if="props.message?.action === 0" class="text-sm italic me-2">{{ props.message?.userid }}</span>
<span class="text-sm italic me-2">{{ formatDateDifference(props.message?.reg_time || props.message?.date || '') }}</span>
</div>
</VCard>
</template>

View File

@@ -49,7 +49,7 @@ async function installPlugin() {
try { try {
// 显示等待提示框 // 显示等待提示框
progressDialog.value = true progressDialog.value = true
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...` progressText.value = `正在安装 ${props.plugin?.plugin_name} v${props?.plugin?.plugin_version} ...`
const result: { [key: string]: any } = await api.get( const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`, `plugin/install/${props.plugin?.id}`,
@@ -163,15 +163,6 @@ const dropdownItems = ref([
</VMenu> </VMenu>
</IconBtn> </IconBtn>
</div> </div>
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 left-1"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
</div>
<VAvatar <VAvatar
size="8rem" size="8rem"
> >

View File

@@ -41,12 +41,18 @@ const pluginConfigDialog = ref(false)
// 插件配置表单数据 // 插件配置表单数据
const pluginConfigForm = ref({}) const pluginConfigForm = ref({})
// 进度框
const progressDialog = ref(false)
// 插件表单配置项 // 插件表单配置项
let pluginFormItems = reactive([]) let pluginFormItems = reactive([])
// 插件数据页面 // 插件数据页面
const pluginInfoDialog = ref(false) const pluginInfoDialog = ref(false)
// 进度框文本
const progressText = ref('正在更新插件...')
// 插件数据页面配置项 // 插件数据页面配置项
let pluginPageItems = reactive([]) let pluginPageItems = reactive([])
@@ -83,7 +89,12 @@ async function uninstallPlugin() {
return return
try { try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`) const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) { if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`) $toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
@@ -221,6 +232,41 @@ async function resetPlugin() {
} }
} }
// 更新插件
async function updatePlugin() {
try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在更新 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`,
{
params: {
repo_url: props.plugin?.repo_url,
force: true,
},
},
)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 更新成功!`)
// 通知父组件刷新
emit('save')
}
else {
$toast.error(`插件 ${props.plugin?.plugin_name} 更新失败:${result.message}`)
}
}
catch (error) {
console.error(error)
}
}
// 访问作者主页 // 访问作者主页
function visitAuthorPage() { function visitAuthorPage() {
window.open(props.plugin?.author_url, '_blank') window.open(props.plugin?.author_url, '_blank')
@@ -254,8 +300,18 @@ const dropdownItems = ref([
}, },
}, },
{ {
title: '重置', title: '更新',
value: 3, value: 3,
show: props.plugin?.has_update,
props: {
prependIcon: 'mdi-cancel',
color: 'success',
click: updatePlugin,
},
},
{
title: '重置',
value: 4,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-cancel', prependIcon: 'mdi-cancel',
@@ -265,7 +321,7 @@ const dropdownItems = ref([
}, },
{ {
title: '卸载', title: '卸载',
value: 4, value: 5,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-trash-can-outline', prependIcon: 'mdi-trash-can-outline',
@@ -275,7 +331,7 @@ const dropdownItems = ref([
}, },
{ {
title: '查看日志', title: '查看日志',
value: 5, value: 6,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-file-document-outline', prependIcon: 'mdi-file-document-outline',
@@ -286,7 +342,7 @@ const dropdownItems = ref([
}, },
{ {
title: '作者主页', title: '作者主页',
value: 5, value: 7,
show: true, show: true,
props: { props: {
prependIcon: 'mdi-home-circle-outline', prependIcon: 'mdi-home-circle-outline',
@@ -294,6 +350,13 @@ const dropdownItems = ref([
}, },
}, },
]) ])
// 监听插件状态变化
watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1)
dropdownItems.value[updateItemIndex].show = newHasUpdate
})
</script> </script>
<template> <template>
@@ -313,6 +376,15 @@ const dropdownItems = ref([
class="relative pa-4 text-center card-cover-blurred" class="relative pa-4 text-center card-cover-blurred"
:style="{ background: `${backgroundColor}` }" :style="{ background: `${backgroundColor}` }"
> >
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 left-1"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
</div>
<div class="me-n3 absolute top-0 right-3"> <div class="me-n3 absolute top-0 right-3">
<IconBtn> <IconBtn>
<VIcon icon="mdi-dots-vertical" class="text-white" /> <VIcon icon="mdi-dots-vertical" class="text-white" />
@@ -430,6 +502,25 @@ const dropdownItems = ref([
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 更新插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -121,6 +121,9 @@ async function updateSiteInfo() {
<template> <template>
<VDialog <VDialog
scrollable scrollable
:close-on-back="false"
persistent
eager
max-width="60rem" max-width="60rem"
> >
<VCard <VCard

View File

@@ -30,6 +30,7 @@ const subscribeForm = ref<Subscribe>({
total_episode: 0, total_episode: 0,
start_episode: 0, start_episode: 0,
best_version: 0, best_version: 0,
search_imdbid: 0,
sites: [], sites: [],
type: '', type: '',
name: '', name: '',
@@ -99,6 +100,7 @@ async function getSubscribeInfo() {
) )
subscribeForm.value = result subscribeForm.value = result
subscribeForm.value.best_version = subscribeForm.value.best_version === 1 subscribeForm.value.best_version = subscribeForm.value.best_version === 1
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
} }
catch (e) { catch (e) {
console.log(e) console.log(e)
@@ -343,6 +345,15 @@ watchEffect(() => {
label="洗版" label="洗版"
/> />
</VCol> </VCol>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="subscribeForm.search_imdbid"
label="使用 ImdbID 搜索"
/>
</VCol>
</VRow> </VRow>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -44,7 +44,7 @@ const superUser = store.state.auth.superUser
</IconBtn> </IconBtn>
<!-- 👉 Shortcuts --> <!-- 👉 Shortcuts -->
<ShortcutBar /> <ShortcutBar v-if="superUser" />
<!-- 👉 Theme --> <!-- 👉 Theme -->
<NavbarThemeSwitcher class="me-2" /> <NavbarThemeSwitcher class="me-2" />

View File

@@ -84,7 +84,7 @@ function openSearchDialog() {
<VTextField <VTextField
key="search_navbar" key="search_navbar"
v-model="searchWord" v-model="searchWord"
class="d-none d-lg-block text-disabled" class="d-none d-lg-block text-disabled search-box"
density="compact" density="compact"
variant="solo" variant="solo"
label="搜索电影、电视剧" label="搜索电影、电视剧"
@@ -98,3 +98,9 @@ function openSearchDialog() {
/> />
</span> </span>
</template> </template>
<style lang="scss">
.search-box div.v-input__control div[role="textbox"] {
border: 1px solid rgb(var(--v-theme-background));
}
</style>

View File

@@ -3,7 +3,10 @@ import NameTestView from '@/views/system/NameTestView.vue'
import NetTestView from '@/views/system/NetTestView.vue' import NetTestView from '@/views/system/NetTestView.vue'
import LoggingView from '@/views/system/LoggingView.vue' import LoggingView from '@/views/system/LoggingView.vue'
import RuleTestView from '@/views/system/RuleTestView.vue' import RuleTestView from '@/views/system/RuleTestView.vue'
import ModuleTestView from '@/views/system/ModuleTestView.vue'
import MessageView from '@/views/system/MessageView.vue'
import store from '@/store' import store from '@/store'
import api from '@/api'
// App捷径 // App捷径
const appsMenu = ref(false) const appsMenu = ref(false)
@@ -20,11 +23,56 @@ const loggingDialog = ref(false)
// 过滤规则弹窗 // 过滤规则弹窗
const ruleTestDialog = ref(false) const ruleTestDialog = ref(false)
// 系统健康检查弹窗
const systemTestDialog = ref(false)
// 消息中心弹窗
const messageDialog = ref(false)
// 输入消息
const user_message = ref('')
// 发送按钮是否可用
const sendButtonDisabled = ref(false)
// 聊天容器
const chatContainer = ref<HTMLDivElement>()
// 滚动到底部
function scrollMessageToEnd() {
nextTick(() => {
if (chatContainer.value) {
const scrollDiv = chatContainer.value.$el
scrollDiv.scrollTop = scrollDiv.scrollHeight
}
})
}
// 拼接全部日志url // 拼接全部日志url
function allLoggingUrl() { function allLoggingUrl() {
const token = store.state.auth.token const token = store.state.auth.token
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1` return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
} }
// 发送消息
async function sendMessage() {
if (user_message.value) {
try {
sendButtonDisabled.value = true
await api.post(`message/web?text=${user_message.value}`)
user_message.value = ''
sendButtonDisabled.value = false
scrollMessageToEnd()
}
catch (error) {
console.error(error)
}
}
}
onMounted(() => {
scrollMessageToEnd()
})
</script> </script>
<template> <template>
@@ -44,11 +92,7 @@ function allLoggingUrl() {
class="me-2" class="me-2"
v-bind="props" v-bind="props"
> >
<VTooltip text="捷径"> <VIcon icon="mdi-checkbox-multiple-blank-outline" />
<template #activator="{ props: _props }">
<VIcon v-bind="_props" icon="mdi-checkbox-multiple-blank-outline" />
</template>
</VTooltip>
</IconBtn> </IconBtn>
</template> </template>
<!-- Menu Content --> <!-- Menu Content -->
@@ -85,23 +129,23 @@ function allLoggingUrl() {
</VCol> </VCol>
<VCol <VCol
cols="6" cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon" class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}" @click="() => {}"
> >
<VListItem <VListItem
class="pa-4" class="pa-4"
@click="netTestDialog = true" @click="ruleTestDialog = true"
> >
<VAvatar <VAvatar
size="48" size="48"
variant="tonal" variant="tonal"
> >
<VIcon icon="mdi-network-outline" /> <VIcon icon="mdi-filter-cog-outline" />
</VAvatar> </VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0"> <h6 class="text-base font-weight-medium mt-2 mb-0">
网络 优先级
</h6> </h6>
<span class="text-sm">测试网速连通性</span> <span class="text-sm">优先级规则测试</span>
</VListItem> </VListItem>
</VCol> </VCol>
</VRow> </VRow>
@@ -124,7 +168,51 @@ function allLoggingUrl() {
<h6 class="text-base font-weight-medium mt-2 mb-0"> <h6 class="text-base font-weight-medium mt-2 mb-0">
日志 日志
</h6> </h6>
<span class="text-sm">系统实时日志</span> <span class="text-sm">实时日志</span>
</VListItem>
</VCol>
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="netTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VIcon icon="mdi-network-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
网络
</h6>
<span class="text-sm">网速连通性测试</span>
</VListItem>
</VCol>
</VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="systemTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VIcon icon="mdi-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
系统
</h6>
<span class="text-sm">健康检查</span>
</VListItem> </VListItem>
</VCol> </VCol>
<VCol <VCol
@@ -134,18 +222,18 @@ function allLoggingUrl() {
> >
<VListItem <VListItem
class="pa-4" class="pa-4"
@click="ruleTestDialog = true" @click="messageDialog = true"
> >
<VAvatar <VAvatar
size="48" size="48"
variant="tonal" variant="tonal"
> >
<VIcon icon="mdi-filter-cog-outline" /> <VIcon icon="mdi-message-outline" />
</VAvatar> </VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0"> <h6 class="text-base font-weight-medium mt-2 mb-0">
优先级 消息
</h6> </h6>
<span class="text-sm">优先级规则测试</span> <span class="text-sm">消息中心</span>
</VListItem> </VListItem>
</VCol> </VCol>
</VRow> </VRow>
@@ -213,4 +301,54 @@ function allLoggingUrl() {
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 系统健康检查弹窗 -->
<VDialog
v-model="systemTestDialog"
max-width="50rem"
scrollable
>
<VCard title="系统健康检查">
<DialogCloseBtn @click="systemTestDialog = false" />
<VCardText>
<ModuleTestView />
</VCardText>
</VCard>
</VDialog>
<!-- 消息中心弹窗 -->
<VDialog
v-model="messageDialog"
max-width="60rem"
scrollable
>
<VCard title="消息中心">
<DialogCloseBtn @click="messageDialog = false" />
<VCardText ref="chatContainer">
<MessageView @scroll="scrollMessageToEnd" />
</VCardText>
<VCardItem>
<VTextField
v-model="user_message"
placeholder="输入消息或命令"
outlined
hide-details
single-line
clearable
density="compact"
:disabled="sendButtonDisabled"
@keydown.enter="sendMessage"
>
<template #append>
<VBtn
color="primary"
:disabled="sendButtonDisabled"
@click="sendMessage"
>
发送
</VBtn>
</template>
</VTextField>
</VCardItem>
</VCard>
</VDialog>
</template> </template>

View File

@@ -1,14 +1,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVerticalNav.vue' import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVerticalNav.vue'
import api from '@/api'
const router = useRouter()
const route = useRoute()
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
api.get('user/current')
.catch(() => {
router.replace('/login')
})
}
})
</script> </script>
<template> <template>
<DefaultLayoutWithVerticalNav> <DefaultLayoutWithVerticalNav>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive>
<component :is="Component" v-if="$route.meta.keepAlive" :key="$route.fullPath" /> <component :is="Component" v-if="route.meta.keepAlive" :key="route.fullPath" />
</keep-alive> </keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" :key="$route.fullPath" /> <component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
</router-view> </router-view>
</DefaultLayoutWithVerticalNav> </DefaultLayoutWithVerticalNav>
</template> </template>

View File

@@ -77,8 +77,8 @@ function login() {
store.dispatch('auth/updateUserName', username) store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar) store.dispatch('auth/updateAvatar', avatar)
// 跳转到首页 // 跳转到首页或回原始页面
router.push('/') router.push(store.state.auth.originalPath ?? '/')
}) })
.catch((error: any) => { .catch((error: any) => {
// 登录失败,显示错误提示 // 登录失败,显示错误提示

View File

@@ -80,6 +80,7 @@ export default {
// set v-rating default color to primary // set v-rating default color to primary
color: 'rgba(var(--v-theme-on-background),0.23)', color: 'rgba(var(--v-theme-on-background),0.23)',
activeColor: 'warning', activeColor: 'warning',
halfIncrements: true,
}, },
VProgressCircular: { VProgressCircular: {
// set v-progress-circular default color to primary // set v-progress-circular default color to primary

View File

@@ -162,6 +162,7 @@ router.beforeEach((to, from, next) => {
const isAuthenticated = store.state.auth.token !== null const isAuthenticated = store.state.auth.token !== null
if (to.meta.requiresAuth && !isAuthenticated) { if (to.meta.requiresAuth && !isAuthenticated) {
store.state.auth.originalPath = to.fullPath
next('/login') next('/login')
} }
else { else {

View File

@@ -7,6 +7,7 @@ interface AuthState {
superUser: boolean superUser: boolean
userName: string userName: string
avatar: string avatar: string
originalPath: string | null
} }
// 定义根状态类型 // 定义根状态类型
@@ -23,6 +24,7 @@ const authModule: Module<AuthState, RootState> = {
superUser: false, superUser: false,
userName: '', userName: '',
avatar: '', avatar: '',
originalPath: null,
}, },
mutations: { mutations: {
setToken(state, token: string) { setToken(state, token: string) {

View File

@@ -68,6 +68,14 @@ function initOptions(data: Context) {
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix) optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
} }
// 对季过滤选项进行排序
const sortSeasonFilterOptions = computed(() => {
return seasonFilterOptions.value.sort((a, b) => {
// 按字符串升序排序
return a.localeCompare(b, 'zh-Hans-CN', { sensitivity: 'accent' })
})
})
// 计算分组后的列表 // 计算分组后的列表
onMounted(() => { onMounted(() => {
// 数据分组 // 数据分组
@@ -154,7 +162,7 @@ watchEffect(() => {
<VCol v-if="seasonFilterOptions.length > 0" cols="6" md=""> <VCol v-if="seasonFilterOptions.length > 0" cols="6" md="">
<VSelect <VSelect
v-model="filterForm.season" v-model="filterForm.season"
:items="seasonFilterOptions" :items="sortSeasonFilterOptions"
size="small" size="small"
density="compact" density="compact"
chips chips

View File

@@ -55,17 +55,37 @@ async function fetchUninstalledPlugins() {
state: 'market', state: 'market',
}, },
}) })
// 设置APP市场加载完成
isAppMarketLoaded.value = true isAppMarketLoaded.value = true
// 设置更新状态
for (const uninstalled of uninstalledList.value) {
for (const data of dataList.value) {
if (uninstalled.id === data.id) {
data.has_update = true
data.repo_url = uninstalled.repo_url
}
}
}
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
} }
} }
// 加载时获取数据 // 加载所有数据
onBeforeMount(() => { function refreshData() {
fetchInstalledPlugins() fetchInstalledPlugins()
fetchUninstalledPlugins() fetchUninstalledPlugins()
}
// 获取没有更新的插件
const getUnupdatedPlugins = computed(() => {
return uninstalledList.value.filter(item => !item.has_update)
})
// 加载时获取数据
onBeforeMount(() => {
refreshData()
}) })
</script> </script>
@@ -89,8 +109,8 @@ onBeforeMount(() => {
v-for="data in dataList" v-for="data in dataList"
:key="data.id" :key="data.id"
:plugin="data" :plugin="data"
@remove="fetchInstalledPlugins" @remove="refreshData"
@save="fetchInstalledPlugins" @save="refreshData"
/> />
</div> </div>
<NoDataFound <NoDataFound
@@ -105,6 +125,7 @@ onBeforeMount(() => {
fullscreen fullscreen
scrollable scrollable
:scrim="false" :scrim="false"
:z-index="1010"
transition="dialog-bottom-transition" transition="dialog-bottom-transition"
> >
<!-- Dialog Activator --> <!-- Dialog Activator -->
@@ -153,7 +174,7 @@ onBeforeMount(() => {
</div> </div>
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card"> <div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
<PluginAppCard <PluginAppCard
v-for="data in uninstalledList" v-for="data in getUnupdatedPlugins"
:key="data.id" :key="data.id"
:plugin="data" :plugin="data"
@install="pluginInstalled" @install="pluginInstalled"

View File

@@ -6,10 +6,6 @@ import NoDataFound from '@/components/NoDataFound.vue'
import DownloadingCard from '@/components/cards/DownloadingCard.vue' import DownloadingCard from '@/components/cards/DownloadingCard.vue'
import store from '@/store' import store from '@/store'
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
// 定时器 // 定时器
let refreshTimer: NodeJS.Timer | null = null let refreshTimer: NodeJS.Timer | null = null
@@ -42,10 +38,13 @@ function onRefresh() {
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅 // 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => { const filteredDataList = computed(() => {
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
if (superUser) if (superUser)
return dataList.value return dataList.value
else else
return dataList.value.filter(data => data.userid === userName) return dataList.value.filter(data => data.userid === userName || data.username === userName)
}) })
// 加载时获取数据 // 加载时获取数据

View File

@@ -29,6 +29,9 @@ const notificationSettings = ref({
SLACK_CHANNEL: '', SLACK_CHANNEL: '',
SYNOLOGYCHAT_WEBHOOK: '', SYNOLOGYCHAT_WEBHOOK: '',
SYNOLOGYCHAT_TOKEN: '', SYNOLOGYCHAT_TOKEN: '',
VOCECHAT_HOST: '',
VOCECHAT_API_KEY: '',
VOCECHAT_CHANNEL_ID: '',
}) })
// 消息渠道 // 消息渠道
@@ -49,6 +52,10 @@ const NotificationChannels = [
title: 'SynologyChat', title: 'SynologyChat',
value: 'synologychat', value: 'synologychat',
}, },
{
title: 'VoceChat',
value: 'vocechat',
},
] ]
// 提示框 // 提示框
@@ -110,6 +117,9 @@ async function loadNotificationSettings() {
SLACK_CHANNEL, SLACK_CHANNEL,
SYNOLOGYCHAT_WEBHOOK, SYNOLOGYCHAT_WEBHOOK,
SYNOLOGYCHAT_TOKEN, SYNOLOGYCHAT_TOKEN,
VOCECHAT_HOST,
VOCECHAT_API_KEY,
VOCECHAT_CHANNEL_ID,
} = result2.data } = result2.data
notificationSettings.value = { notificationSettings.value = {
WECHAT_CORPID, WECHAT_CORPID,
@@ -128,6 +138,9 @@ async function loadNotificationSettings() {
SLACK_CHANNEL, SLACK_CHANNEL,
SYNOLOGYCHAT_WEBHOOK, SYNOLOGYCHAT_WEBHOOK,
SYNOLOGYCHAT_TOKEN, SYNOLOGYCHAT_TOKEN,
VOCECHAT_HOST,
VOCECHAT_API_KEY,
VOCECHAT_CHANNEL_ID,
} }
} }
} }
@@ -217,6 +230,9 @@ onMounted(() => {
<VTab value="synologychat"> <VTab value="synologychat">
SynologyChat SynologyChat
</VTab> </VTab>
<VTab value="vocechat">
VoceChat
</VTab>
</VTabs> </VTabs>
<VWindow <VWindow
v-model="messagerTab" v-model="messagerTab"
@@ -347,6 +363,31 @@ onMounted(() => {
</VRow> </VRow>
</VForm> </VForm>
</VWindowItem> </VWindowItem>
<VWindowItem value="vocechat">
<VForm>
<VRow>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_HOST"
label="地址"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_API_KEY"
label="机器人密钥"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.VOCECHAT_CHANNEL_ID"
label="频道ID"
placeholder="不包含#号"
/>
</VCol>
</VRow>
</VForm>
</VWindowItem>
</VWindow> </VWindow>
</VCol> </VCol>
</VRow> </VRow>
@@ -389,6 +430,9 @@ onMounted(() => {
<th scope="col"> <th scope="col">
SynologyChat SynologyChat
</th> </th>
<th scope="col">
VoceChat
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -411,10 +455,13 @@ onMounted(() => {
<td> <td>
<VCheckbox v-model="message.synologychat" /> <VCheckbox v-model="message.synologychat" />
</td> </td>
<td>
<VCheckbox v-model="message.vocechat" />
</td>
</tr> </tr>
<tr v-if="messagemTypes.length === 0"> <tr v-if="messagemTypes.length === 0">
<td <td
colspan="5" colspan="6"
class="text-center" class="text-center"
> >
没有设置任何通知渠道 没有设置任何通知渠道

View File

@@ -41,6 +41,7 @@ const defaultFilterRules = ref({
exclude: '', exclude: '',
movie_size: '', movie_size: '',
tv_size: '', tv_size: '',
min_seeders: 0,
show_edit_dialog: false, show_edit_dialog: false,
}) })
@@ -589,7 +590,7 @@ onMounted(() => {
label="排除(关键字、正则式)" label="排除(关键字、正则式)"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="defaultFilterRules.movie_size" v-model="defaultFilterRules.movie_size"
type="text" type="text"
@@ -597,7 +598,7 @@ onMounted(() => {
placeholder="0-30" placeholder="0-30"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4">
<VTextField <VTextField
v-model="defaultFilterRules.tv_size" v-model="defaultFilterRules.tv_size"
type="text" type="text"
@@ -605,6 +606,14 @@ onMounted(() => {
placeholder="0-10" placeholder="0-10"
/> />
</VCol> </VCol>
<VCol cols="12" md="4">
<VTextField
v-model="defaultFilterRules.min_seeders"
type="text"
label="最小做种数"
placeholder="0"
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="defaultFilterRules.show_edit_dialog" v-model="defaultFilterRules.show_edit_dialog"

View File

@@ -3,10 +3,14 @@
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { VRow } from 'vuetify/lib/components/index.mjs' import { VRow } from 'vuetify/lib/components/index.mjs'
import api from '@/api' import api from '@/api'
import { requiredValidator } from '@/@validators'
// 选中的媒体服务器 // 选中的媒体服务器
const selectedMediaServers = ref([]) const selectedMediaServers = ref([])
// 选中的下载器
const selectedDownloaders = ref([])
// 下载器选中标签页 // 下载器选中标签页
const downloaderTab = ref('qbittorrent') const downloaderTab = ref('qbittorrent')
@@ -32,7 +36,6 @@ const mediaSettings = ref({
// 下载器设置项 // 下载器设置项
const downloaderSettings = ref({ const downloaderSettings = ref({
DOWNLOADER: '',
DOWNLOADER_MONITOR: true, DOWNLOADER_MONITOR: true,
TORRENT_TAG: '', TORRENT_TAG: '',
QB_HOST: '', QB_HOST: '',
@@ -183,10 +186,13 @@ async function saveMediaSetting() {
// 调用API查询下载器设置 // 调用API查询下载器设置
async function loadDownladerSetting() { async function loadDownladerSetting() {
try { try {
const result: { [key: string]: any } = await api.get('system/env') const result1: { [key: string]: any } = await api.get('system/setting/DOWNLOADER')
if (result.success) { if (result1.success)
selectedDownloaders.value = result1.data?.value?.split(',')
const result2: { [key: string]: any } = await api.get('system/env')
if (result2.success) {
const { const {
DOWNLOADER,
DOWNLOADER_MONITOR, DOWNLOADER_MONITOR,
TORRENT_TAG, TORRENT_TAG,
QB_HOST, QB_HOST,
@@ -198,9 +204,8 @@ async function loadDownladerSetting() {
TR_HOST, TR_HOST,
TR_USER, TR_USER,
TR_PASSWORD, TR_PASSWORD,
} = result.data } = result2.data
downloaderSettings.value = { downloaderSettings.value = {
DOWNLOADER,
DOWNLOADER_MONITOR, DOWNLOADER_MONITOR,
TORRENT_TAG, TORRENT_TAG,
QB_HOST, QB_HOST,
@@ -223,12 +228,16 @@ async function loadDownladerSetting() {
// 调用API保存下载器设置 // 调用API保存下载器设置
async function saveDownloaderSetting() { async function saveDownloaderSetting() {
try { try {
const result: { [key: string]: any } = await api.post( const result1: { [key: string]: any } = await api.post(
'system/setting/DOWNLOADER',
selectedDownloaders.value.join(','),
)
const result2: { [key: string]: any } = await api.post(
'system/env', 'system/env',
downloaderSettings.value, downloaderSettings.value,
) )
if (result.success) { if (result1.success && result2.success) {
$toast.success('保存下载器设置成功') $toast.success('保存下载器设置成功')
reloadModule() reloadModule()
} }
@@ -331,13 +340,15 @@ onMounted(() => {
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard title="下载器"> <VCard title="下载器">
<VCardSubtitle>只有选中的下载器才会被默认使用</VCardSubtitle> <VCardSubtitle>只有选中的第1个下载器才会被默认使用</VCardSubtitle>
<VCardText> <VCardText>
<VForm> <VForm>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSelect <VSelect
v-model="downloaderSettings.DOWNLOADER" v-model="selectedDownloaders"
multiple
chips
:items="Downloaders" :items="Downloaders"
label="当前使用下载器" label="当前使用下载器"
/> />
@@ -353,7 +364,7 @@ onMounted(() => {
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="downloaderSettings.DOWNLOADER_MONITOR" v-model="downloaderSettings.DOWNLOADER_MONITOR"
label="监控下载器" label="监控默认下载器"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -628,6 +639,7 @@ onMounted(() => {
<VTextField <VTextField
v-model="mediaSettings.DOWNLOAD_PATH" v-model="mediaSettings.DOWNLOAD_PATH"
label="下载目录" label="下载目录"
:rules="[requiredValidator]"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -682,6 +694,8 @@ onMounted(() => {
<VTextField <VTextField
v-model="mediaSettings.LIBRARY_PATH" v-model="mediaSettings.LIBRARY_PATH"
label="媒体库目录" label="媒体库目录"
placeholder="多个目录使用,分隔"
:rules="[requiredValidator]"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">

View File

@@ -68,6 +68,7 @@ onBeforeMount(fetchData)
@click="siteAddDialog = true" @click="siteAddDialog = true"
/> />
<SiteAddEditForm <SiteAddEditForm
v-if="siteAddDialog"
v-model="siteAddDialog" v-model="siteAddDialog"
oper="add" oper="add"
@save="siteAddDialog = false; fetchData()" @save="siteAddDialog = false; fetchData()"

View File

@@ -20,6 +20,7 @@ const calendarOptions: Ref<CalendarOptions> = ref({
], ],
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
weekends: true, weekends: true,
firstDay: 1,
headerToolbar: { headerToolbar: {
left: 'prev', left: 'prev',
center: 'title', center: 'title',
@@ -197,6 +198,11 @@ onMounted(() => {
--fc-event-border-color: currentcolor; --fc-event-border-color: currentcolor;
} }
// 当天背景渐变
.fc-day-today {
background-image: linear-gradient(to bottom, #AF85FD ,rgba(var(--v-theme-on-surface), 0.04));
}
.v-application .fc a { .v-application .fc a {
color: inherit; color: inherit;
} }

View File

@@ -11,10 +11,6 @@ const props = defineProps({
type: String, type: String,
}) })
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
// 是否刷新过 // 是否刷新过
const isRefreshed = ref(false) const isRefreshed = ref(false)
@@ -47,10 +43,13 @@ function onRefresh() {
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅 // 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => { const filteredDataList = computed(() => {
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
if (superUser) if (superUser)
return dataList.value.filter(data => data.type === props.type) return dataList.value.filter(data => data.type === props.type)
else else
return dataList.value.filter(data => data.type === props.type && data.username === userName) return dataList.value.filter(data => data.type === props.type && (data.username === userName))
}) })
</script> </script>

View File

@@ -34,7 +34,7 @@ function extractLogDetailsFromLogs(logs: string[]): { level: string; time: strin
const matches = RegExp(logPattern).exec(log) const matches = RegExp(logPattern).exec(log)
if (matches && matches.length === 5) { if (matches && matches.length === 5) {
const [_, level, time, program, content] = matches const [_, level, time, program, content] = matches
logDetails.push({ level, time, program, content }) logDetails.unshift({ level, time, program, content })
} }
} }

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import store from '@/store'
import type { Message } from '@/api/types'
import MessageCard from '@/components/cards/MessageCard.vue'
import api from '@/api'
// 定义事件
const emit = defineEmits(['scroll'])
// 消息列表
const messages = ref<Message[]>([])
// 当前页数据
const currData = ref<Message[]>([])
// 是否完成加载
const isLoaded = ref(false)
// 是否加载中
const loading = ref(false)
// 当前页码
const page = ref(1)
// SSE持续获取消息
function startSSEMessager() {
const token = store.state.auth.token
if (token) {
const eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}&role=user`,
)
eventSource.addEventListener('message', (event) => {
const message = event.data
if (message) {
const object = JSON.parse(message)
messages.value.push(object)
emit('scroll')
}
})
onBeforeUnmount(() => {
eventSource.close()
})
}
}
// 调用API加载存量消息
async function loadMessages({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 设置加载中
loading.value = true
try {
currData.value = await api.get('message/web', {
params: {
page: page.value,
size: 20,
},
})
if (currData.value.length > 0) {
// 合并数据
messages.value = [...currData.value, ...messages.value]
// 加载完成
done('ok')
if (page.value === 1) {
// 滚动到底部
emit('scroll')
}
// 页码+1
page.value++
}
else {
done('ok')
}
loading.value = false
isLoaded.value = true
}
catch (error) {
console.error(error)
}
}
onMounted(() => {
// 监听新消息
startSSEMessager()
})
</script>
<template>
<VInfiniteScroll
mode="intersect"
side="start"
:items="messages"
class="overflow-hidden"
@load="loadMessages"
>
<template #loading>
<VProgressCircular
v-if="loading"
indeterminate
size="48"
class="mb-5"
color="primary"
/>
</template>
<div>
<VRow
v-for="(msg, index) in messages"
:key="index"
:class="{
'justify-end': msg.action === 0,
'justify-start': msg.action === 1,
}"
>
<VCol
cols="10"
lg="6"
xl="4"
style="position: relative;"
>
<MessageCard
:message="msg"
/>
</VCol>
</VRow>
</div>
<div
v-if="messages.length === 0 && isLoaded && !loading"
class="w-full text-center flex flex-col items-center"
>
<span class="mb-3">当前没有消息</span>
</div>
</VInfiniteScroll>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import api from '@/api'
// 定义所有的模块ID、名称列表
const modules = ref([
{ id: 'FileTransferModule', name: '媒体目录', state: '', errmsg: '', loading: false },
{ id: 'IndexerModule', name: '站点索引', state: '', errmsg: '', loading: false },
{ id: 'DoubanModule', name: '豆瓣', state: '', errmsg: '', loading: false },
{ id: 'TheMovieDbModule', name: 'TheMovieDb', state: '', errmsg: '', loading: false },
{ id: 'TheTvDbModule', name: 'TheTvDb', state: '', errmsg: '', loading: false },
{ id: 'FanartModule', name: 'Fanart', state: '', errmsg: '', loading: false },
{ id: 'EmbyModule', name: 'Emby', state: '', errmsg: '', loading: false },
{ id: 'JellyfinModule', name: 'Jellyfin', state: '', errmsg: '', loading: false },
{ id: 'PlexModule', name: 'Plex', state: '', errmsg: '', loading: false },
{ id: 'WechatModule', name: '微信', state: '', errmsg: '', loading: false },
{ id: 'TelegramModule', name: 'Telegram', state: '', errmsg: '', loading: false },
{ id: 'SlackModule', name: 'Slack', state: '', errmsg: '', loading: false },
{ id: 'SynologyChatModule', name: 'Synology Chat', state: '', errmsg: '', loading: false },
{ id: 'VoceChatModule', name: 'VoceChat', state: '', errmsg: '', loading: false },
{ id: 'QbittorrentModule', name: 'Qbittorrent', state: '', errmsg: '', loading: false },
{ id: 'TransmissionModule', name: 'Transmission', state: '', errmsg: '', loading: false },
])
// 调用API测试模块
async function moduleTest(index: number) {
try {
const target = modules.value[index]
const moduleid = target.id
target.loading = true
const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`)
target.loading = false
if (result.success) {
target.state = 'success'
target.name = `${target.name} - 正常`
}
else if (result.message?.includes('模块未加载')) {
target.state = ''
target.name = `${target.name} - 未启用`
}
else {
target.state = 'error'
target.name = `${target.name} - 错误!`
target.errmsg = result.message
}
}
catch (error) {
console.error(error)
}
}
// 加载
onMounted(async () => {
// 逐个检查所有模块
for (let i = 0; i < modules.value.length; i++)
await moduleTest(i)
})
</script>
<template>
<VAlert
v-for="(module, index) in modules"
:key="index"
:type="module.state"
:title="module.name"
class="mb-2"
variant="tonal"
>
{{ module.errmsg }}
<template #append>
<VProgressCircular
v-if="module.loading"
indeterminate
/>
</template>
</VAlert>
</template>

View File

@@ -6,6 +6,8 @@ import slack from '@images/logos/slack.png'
import telegram from '@images/logos/telegram.webp' import telegram from '@images/logos/telegram.webp'
import tmdb from '@images/logos/tmdb.png' import tmdb from '@images/logos/tmdb.png'
import wechat from '@images/logos/wechat.png' import wechat from '@images/logos/wechat.png'
import fanart from '@images/logos/fanart.webp'
import tvdb from '@images/logos/thetvdb.jpeg'
interface Status { interface Status {
OK: string OK: string
@@ -57,6 +59,26 @@ const targets = ref<Address[]>([
message: '未测试', message: '未测试',
btndisable: false, btndisable: false,
}, },
{
image: tvdb,
name: 'api.thetvdb.com',
url: 'https://api.thetvdb.com/series/81189',
proxy: true,
status: 'Normal',
time: '',
message: '未测试',
btndisable: false,
},
{
image: fanart,
name: 'webservice.fanart.tv',
url: 'https://webservice.fanart.tv',
proxy: true,
status: 'Normal',
time: '',
message: '未测试',
btndisable: false,
},
{ {
image: telegram, image: telegram,
name: 'api.telegram.org', name: 'api.telegram.org',

View File

@@ -54,6 +54,12 @@ export default defineConfig({
build: { build: {
chunkSizeWarningLimit: 5000, chunkSizeWarningLimit: 5000,
cssCodeSplit: false, cssCodeSplit: false,
rollupOptions: {
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
},
},
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['vuetify'], exclude: ['vuetify'],