mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
Merge pull request #416 from PKC278/v2
This commit is contained in:
+1
-102
@@ -250,108 +250,7 @@
|
|||||||
<div id="loading-bg">
|
<div id="loading-bg">
|
||||||
<div class="loading-logo">
|
<div class="loading-logo">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<svg width="160px" height="160px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
<img src="/logo.svg" alt="MoviePilot" width="160px" height="160px" />
|
||||||
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
|
|
||||||
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" />
|
|
||||||
</clipPath>
|
|
||||||
<g clip-path="url(#_clip5)">
|
|
||||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
|
||||||
<path
|
|
||||||
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)" />
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
|
||||||
<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"
|
|
||||||
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>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
|
||||||
<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" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
|
||||||
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="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
|
||||||
<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="_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>
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<div class="effect-1 effects"></div>
|
<div class="effect-1 effects"></div>
|
||||||
|
|||||||
Vendored
+1
@@ -12,3 +12,4 @@ declare module 'vue-prism-component' {
|
|||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
declare module 'vue-shepherd';
|
declare module 'vue-shepherd';
|
||||||
|
declare module 'colorthief';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@use "sass:map";
|
@use "sass:map";
|
||||||
@use "vuetify/lib/styles/settings" as vuetify_settings;
|
@use "vuetify/lib/styles/settings/_index.sass" as vuetify_settings;
|
||||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||||
|
|
||||||
@mixin themed($property, $light-value, $dark-value) {
|
@mixin themed($property, $light-value, $dark-value) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ValidationRule } from 'vuetify/types/services/validation'
|
type ValidationRule = (value: any) => string | boolean
|
||||||
|
|
||||||
// 必输校验
|
// 必输校验
|
||||||
export const requiredValidator: ValidationRule = (value: any) => {
|
export const requiredValidator: ValidationRule = (value: any) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import FileToolbar from './filebrowser/FileToolbar.vue'
|
|||||||
import FileNavigator from './filebrowser/FileNavigator.vue'
|
import FileNavigator from './filebrowser/FileNavigator.vue'
|
||||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
||||||
import { storageIconDict } from '@/api/constants'
|
import { storageIconDict } from '@/api/constants'
|
||||||
|
import type { AxiosInstance } from 'axios'
|
||||||
|
|
||||||
// LocalStorage keys
|
// LocalStorage keys
|
||||||
const SORT_KEY = 'fileBrowser.sort'
|
const SORT_KEY = 'fileBrowser.sort'
|
||||||
@@ -16,7 +17,7 @@ const props = defineProps({
|
|||||||
tree: Boolean,
|
tree: Boolean,
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
type: Function,
|
type: Object as PropType<AxiosInstance>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
axiosconfig: Object,
|
axiosconfig: Object,
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { clearCachesAndServiceWorker } from '@/composables/useVersionChecker'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
declare const __APP_VERSION__: string
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// APP版本
|
||||||
|
const appVersion = __APP_VERSION__
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
@@ -115,6 +121,13 @@ function releaseTime(releaseDate: string) {
|
|||||||
return formatDateDifference(releaseDate)
|
return formatDateDifference(releaseDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 强制清除缓存
|
||||||
|
async function cleanCache() {
|
||||||
|
await clearCachesAndServiceWorker()
|
||||||
|
// 刷新页面
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
querySystemEnv()
|
querySystemEnv()
|
||||||
queryAllRelease()
|
queryAllRelease()
|
||||||
@@ -170,6 +183,27 @@ onMounted(() => {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
|
<dt class="block text-sm font-bold">{{ t('setting.about.browserVersion') }}</dt>
|
||||||
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
|
<span class="flex-grow flex flex-row items-center truncate">
|
||||||
|
<code class="truncate">{{ appVersion }}</code>
|
||||||
|
<VBtn
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
class="ms-2"
|
||||||
|
@click="cleanCache"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-refresh" size="14" />
|
||||||
|
</template>
|
||||||
|
{{ t('setting.about.cleanCache') }}
|
||||||
|
</VBtn>
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.authVersion') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.authVersion') }}</dt>
|
||||||
@@ -194,7 +228,7 @@ onMounted(() => {
|
|||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.configDir') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.configDir') }}</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow undefined">
|
<span class="flex-grow break-all">
|
||||||
<code>{{ systemEnv.CONFIG_DIR }}</code>
|
<code>{{ systemEnv.CONFIG_DIR }}</code>
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -202,7 +236,7 @@ onMounted(() => {
|
|||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.dataDir') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.dataDir') }}</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow undefined"
|
<span class="flex-grow break-all"
|
||||||
><code>{{ t('setting.about.dataDirectory') }}</code></span
|
><code>{{ t('setting.about.dataDirectory') }}</code></span
|
||||||
>
|
>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -212,7 +246,7 @@ onMounted(() => {
|
|||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.timezone') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.timezone') }}</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow undefined">
|
<span class="flex-grow break-all">
|
||||||
<code>{{ systemEnv.TZ }}</code>
|
<code>{{ systemEnv.TZ }}</code>
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -261,7 +295,7 @@ onMounted(() => {
|
|||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.documentation') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.documentation') }}</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow undefined">
|
<span class="flex-grow break-all">
|
||||||
<a
|
<a
|
||||||
href="https://movie-pilot.org"
|
href="https://movie-pilot.org"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -278,7 +312,7 @@ onMounted(() => {
|
|||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.feedback') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.feedback') }}</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow undefined">
|
<span class="flex-grow break-all">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/jxxghp/MoviePilot/issues/new/choose"
|
href="https://github.com/jxxghp/MoviePilot/issues/new/choose"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -295,7 +329,7 @@ onMounted(() => {
|
|||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">{{ t('setting.about.channel') }}</dt>
|
<dt class="block text-sm font-bold">{{ t('setting.about.channel') }}</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow undefined">
|
<span class="flex-grow break-all">
|
||||||
<a
|
<a
|
||||||
href="https://t.me/moviepilot_channel"
|
href="https://t.me/moviepilot_channel"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
@@ -28,7 +28,7 @@ const inProps = defineProps({
|
|||||||
icons: Object,
|
icons: Object,
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
type: Function,
|
type: Object as PropType<AxiosInstance>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
refreshpending: Boolean,
|
refreshpending: Boolean,
|
||||||
@@ -196,7 +196,7 @@ async function list_files() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const data = (await inProps.axios.request(config)) ?? []
|
const data = ((await inProps.axios.request<FileItem[], FileItem[]>(config))) ?? []
|
||||||
// 如果当前路径已经变化,则放弃此次加载结果
|
// 如果当前路径已经变化,则放弃此次加载结果
|
||||||
if (prevURI !== takeURISnapshot()) {
|
if (prevURI !== takeURISnapshot()) {
|
||||||
return;
|
return;
|
||||||
@@ -300,7 +300,7 @@ async function download(item: FileItem) {
|
|||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const result: Blob = await inProps.axios.request(config)
|
const result: Blob = (await inProps.axios.request<Blob, Blob>(config))
|
||||||
if (result) {
|
if (result) {
|
||||||
const downloadUrl = URL.createObjectURL(result)
|
const downloadUrl = URL.createObjectURL(result)
|
||||||
window.open(downloadUrl, '_blank')
|
window.open(downloadUrl, '_blank')
|
||||||
@@ -318,7 +318,7 @@ async function getImgLink(item: FileItem) {
|
|||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
}
|
}
|
||||||
// 加载二进制数据
|
// 加载二进制数据
|
||||||
const result: Blob = await inProps.axios.request(config)
|
const result: Blob = (await inProps.axios.request<Blob, Blob>(config))
|
||||||
if (result) {
|
if (result) {
|
||||||
// 创建图片地址
|
// 创建图片地址
|
||||||
currentImgLink.value = URL.createObjectURL(result)
|
currentImgLink.value = URL.createObjectURL(result)
|
||||||
@@ -395,7 +395,7 @@ async function rename() {
|
|||||||
method: inProps.endpoints?.rename.method || 'post',
|
method: inProps.endpoints?.rename.method || 'post',
|
||||||
data: currentItem.value,
|
data: currentItem.value,
|
||||||
}
|
}
|
||||||
const result: { [key: string]: any } = await inProps.axios?.request(config)
|
const result: { [key: string]: any } = (await inProps.axios?.request<any, { [key: string]: any }>(config))
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
$toast.error(result.message)
|
$toast.error(result.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import type { FileItem } from '@/api/types'
|
import type { FileItem } from '@/api/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
endpoints: Object,
|
endpoints: Object,
|
||||||
axios: {
|
axios: {
|
||||||
type: Function,
|
type: Object as PropType<AxiosInstance>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -131,7 +131,7 @@ async function loadSubdirectories(path: string) {
|
|||||||
data: fakeItem,
|
data: fakeItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await props.axios?.request(config)
|
const result = (await props.axios?.request(config))
|
||||||
if (result && Array.isArray(result)) {
|
if (result && Array.isArray(result)) {
|
||||||
// 过滤出目录项
|
// 过滤出目录项
|
||||||
const dirs = result.filter(item => item.type === 'dir')
|
const dirs = result.filter(item => item.type === 'dir')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||||
import type { EndPoints, FileItem } from '@/api/types'
|
import type { EndPoints, FileItem } from '@/api/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -23,7 +23,7 @@ const inProps = defineProps({
|
|||||||
},
|
},
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
type: Function,
|
type: Object as PropType<AxiosInstance>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
sort: {
|
sort: {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="version-update-toast">
|
||||||
|
<span class="message">{{ message }}</span>
|
||||||
|
<button class="refresh-button" @click="handleRefresh">
|
||||||
|
{{ refreshText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 接收 props
|
||||||
|
interface Props {
|
||||||
|
message: string
|
||||||
|
refreshText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.version-update-toast {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button {
|
||||||
|
padding: 6px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,6 +12,16 @@ const globalPwaStatus = ref<{
|
|||||||
const globalLoading = ref(false)
|
const globalLoading = ref(false)
|
||||||
let initPromise: Promise<void> | null = null
|
let initPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
// UI模式设置
|
||||||
|
export type UIMode = 'auto' | 'desktop' | 'app'
|
||||||
|
const uiMode = ref<UIMode>((localStorage.getItem('ui-mode') as UIMode) || 'auto')
|
||||||
|
|
||||||
|
// 设置UI模式
|
||||||
|
function setUIMode(mode: UIMode) {
|
||||||
|
uiMode.value = mode
|
||||||
|
localStorage.setItem('ui-mode', mode)
|
||||||
|
}
|
||||||
|
|
||||||
// 全局初始化函数
|
// 全局初始化函数
|
||||||
async function initializePWAGlobally() {
|
async function initializePWAGlobally() {
|
||||||
if (initPromise) return initPromise
|
if (initPromise) return initPromise
|
||||||
@@ -50,6 +60,8 @@ export function usePWA() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const appMode = computed(() => {
|
const appMode = computed(() => {
|
||||||
|
if (uiMode.value === 'app') return true
|
||||||
|
if (uiMode.value === 'desktop') return false
|
||||||
return pwaMode.value && display.mdAndDown.value
|
return pwaMode.value && display.mdAndDown.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -70,6 +82,8 @@ export function usePWA() {
|
|||||||
pwaMode,
|
pwaMode,
|
||||||
appMode,
|
appMode,
|
||||||
pwaStatus,
|
pwaStatus,
|
||||||
|
uiMode,
|
||||||
|
setUIMode,
|
||||||
loading: globalLoading,
|
loading: globalLoading,
|
||||||
initializePWA: initializePWAGlobally,
|
initializePWA: initializePWAGlobally,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,16 +236,15 @@ export function usePullDownGesture(options: PullDownOptions = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PWA状态确定后,一次性决定是否添加事件监听器
|
// 监听 appMode 变化动态添加/移除事件监听器
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 等待PWA检测完成后添加事件监听器
|
watch(
|
||||||
const stopWatcher = watch(
|
|
||||||
appMode,
|
appMode,
|
||||||
newValue => {
|
newValue => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
addEventListeners()
|
addEventListeners()
|
||||||
// PWA状态确定后停止监听
|
} else {
|
||||||
stopWatcher()
|
removeEventListeners()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { ref, computed, h } from 'vue'
|
||||||
|
import { useToast } from 'vue-toastification'
|
||||||
|
import i18n from '@/plugins/i18n'
|
||||||
|
import VersionUpdateToast from '@/components/toast/VersionUpdateToast.vue'
|
||||||
|
|
||||||
|
// 声明全局变量类型
|
||||||
|
declare const __APP_VERSION__: string
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const currentVersion = ref(__APP_VERSION__)
|
||||||
|
const serverVersion = ref<string | null>(null)
|
||||||
|
const versionChecked = ref(false)
|
||||||
|
const needsUpdate = computed(() => {
|
||||||
|
return serverVersion.value !== null && serverVersion.value !== currentVersion.value
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存和 Service Worker
|
||||||
|
*/
|
||||||
|
export const clearCachesAndServiceWorker = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// 1. 清除所有缓存
|
||||||
|
if ('caches' in window) {
|
||||||
|
const cacheNames = await caches.keys()
|
||||||
|
await Promise.all(cacheNames.map(name => caches.delete(name)))
|
||||||
|
console.log('[VersionChecker] 已清除所有缓存')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 注销 Service Worker
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||||
|
await Promise.all(registrations.map(registration => registration.unregister()))
|
||||||
|
console.log('[VersionChecker] 已注销所有 Service Worker')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[VersionChecker] 清除缓存失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本检查 Composable
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 检查前端版本与服务端版本是否一致
|
||||||
|
* - 检测到版本更新时清除缓存和 Service Worker
|
||||||
|
* - 显示持久化更新通知
|
||||||
|
*/
|
||||||
|
export function useVersionChecker() {
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示版本更新通知(带刷新按钮)
|
||||||
|
*/
|
||||||
|
const showUpdateNotification = (): void => {
|
||||||
|
// 使用自定义 Vue 组件作为 toast 内容,传递翻译后的文本作为 props
|
||||||
|
const component = h(VersionUpdateToast, {
|
||||||
|
message: i18n.global.t('common.newVersionAvailable'),
|
||||||
|
refreshText: i18n.global.t('common.refresh'),
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.info(component, {
|
||||||
|
timeout: false, // 不自动消失
|
||||||
|
closeButton: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
draggable: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查版本并在需要时显示更新通知
|
||||||
|
* @param latestVersion 服务端返回的最新版本号
|
||||||
|
*/
|
||||||
|
const checkVersion = async (latestVersion: string): Promise<void> => {
|
||||||
|
// 如果已经检查过,则跳过
|
||||||
|
if (versionChecked.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新服务端版本
|
||||||
|
serverVersion.value = latestVersion
|
||||||
|
|
||||||
|
// 版本不同,且尚未显示通知
|
||||||
|
if (needsUpdate.value) {
|
||||||
|
versionChecked.value = true
|
||||||
|
console.log(`[VersionChecker] 检测到版本更新: ${currentVersion.value} -> ${latestVersion}`)
|
||||||
|
|
||||||
|
// 清除缓存和 Service Worker
|
||||||
|
await clearCachesAndServiceWorker()
|
||||||
|
|
||||||
|
// 显示持久化通知
|
||||||
|
showUpdateNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置版本检查状态(用于测试或特殊场景)
|
||||||
|
*/
|
||||||
|
const resetVersionCheck = (): void => {
|
||||||
|
versionChecked.value = false
|
||||||
|
serverVersion.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
currentVersion: computed(() => currentVersion.value),
|
||||||
|
serverVersion: computed(() => serverVersion.value),
|
||||||
|
needsUpdate,
|
||||||
|
versionChecked: computed(() => versionChecked.value),
|
||||||
|
// 方法
|
||||||
|
checkVersion,
|
||||||
|
resetVersionCheck,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -328,6 +328,7 @@ const showDynamicButton = computed(() => {
|
|||||||
block-size: auto;
|
block-size: auto;
|
||||||
inline-size: auto;
|
inline-size: auto;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
|
max-width: 60px; // 限制最大宽度以便动画
|
||||||
|
|
||||||
.footer-card-content {
|
.footer-card-content {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
@@ -358,14 +359,12 @@ const showDynamicButton = computed(() => {
|
|||||||
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-enter-from {
|
.fade-slide-enter-from,
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-slide-leave-to {
|
.fade-slide-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
|
max-width: 0;
|
||||||
|
margin-inline-start: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { saveLocalTheme } from '@/@core/utils/theme'
|
|||||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { themeManager } from '@/utils/themeManager'
|
import { themeManager } from '@/utils/themeManager'
|
||||||
|
import { usePWA, type UIMode } from '@/composables/usePWA'
|
||||||
|
|
||||||
// 认证 Store
|
// 认证 Store
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -27,6 +28,8 @@ const globalSettingsStore = useGlobalSettingsStore()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
// 显示器
|
// 显示器
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
// PWA
|
||||||
|
const { uiMode, setUIMode } = usePWA()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
@@ -40,6 +43,9 @@ const siteAuthDialog = ref(false)
|
|||||||
// 自定义CSS弹窗
|
// 自定义CSS弹窗
|
||||||
const cssDialog = ref(false)
|
const cssDialog = ref(false)
|
||||||
|
|
||||||
|
// UI模式菜单是否显示
|
||||||
|
const showUIModeMenu = ref(false)
|
||||||
|
|
||||||
// 主题菜单是否显示
|
// 主题菜单是否显示
|
||||||
const showThemeMenu = ref(false)
|
const showThemeMenu = ref(false)
|
||||||
|
|
||||||
@@ -233,6 +239,37 @@ const isAdvancedMode = computed(() => {
|
|||||||
return globalSettingsStore.get('ADVANCED_MODE') !== false
|
return globalSettingsStore.get('ADVANCED_MODE') !== false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// UI模式相关
|
||||||
|
const uiModes = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'auto',
|
||||||
|
title: t('theme.autoUI'),
|
||||||
|
icon: 'mdi-devices',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'desktop',
|
||||||
|
title: t('pwa.platforms.desktop'),
|
||||||
|
icon: 'mdi-monitor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app',
|
||||||
|
title: t('pwa.platforms.mobile'),
|
||||||
|
icon: 'mdi-cellphone',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 切换UI模式
|
||||||
|
function changeUIMode(mode: UIMode) {
|
||||||
|
setUIMode(mode)
|
||||||
|
showUIModeMenu.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前UI模式图标
|
||||||
|
const getUIModeIcon = computed(() => {
|
||||||
|
const mode = uiModes.value.find(m => m.name === uiMode.value)
|
||||||
|
return mode?.icon || 'mdi-devices'
|
||||||
|
})
|
||||||
|
|
||||||
// 主题相关功能
|
// 主题相关功能
|
||||||
const { name: themeName, global: globalTheme } = useTheme()
|
const { name: themeName, global: globalTheme } = useTheme()
|
||||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||||
@@ -546,6 +583,41 @@ onUnmounted(() => {
|
|||||||
<VListItemTitle>{{ t('user.siteAuth') }}</VListItemTitle>
|
<VListItemTitle>{{ t('user.siteAuth') }}</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
|
|
||||||
|
<!-- 👉 UI模式设置 - 使用嵌套菜单 -->
|
||||||
|
<VMenu location="end" offset-x min-width="200" v-model="showUIModeMenu" :close-on-content-click="true">
|
||||||
|
<template v-slot:activator="{ props: menuProps }">
|
||||||
|
<VListItem v-bind="menuProps" class="mb-1 rounded-lg" hover>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon :icon="getUIModeIcon" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ t('common.uiMode') }}</VListItemTitle>
|
||||||
|
<VListItemSubtitle>
|
||||||
|
{{ uiModes.find(m => m.name === uiMode)?.title || t('theme.autoUI') }}
|
||||||
|
</VListItemSubtitle>
|
||||||
|
<template #append>
|
||||||
|
<VIcon icon="mdi-chevron-right" size="small" />
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</template>
|
||||||
|
<VList>
|
||||||
|
<VListItem
|
||||||
|
v-for="mode in uiModes"
|
||||||
|
:key="mode.name"
|
||||||
|
@click="changeUIMode(mode.name as UIMode)"
|
||||||
|
:active="uiMode === mode.name"
|
||||||
|
class="mb-1"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon :icon="mode.icon" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ mode.title }}</VListItemTitle>
|
||||||
|
<template #append v-if="uiMode === mode.name">
|
||||||
|
<VIcon icon="mdi-check" color="primary" size="small" />
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
|
||||||
<!-- 👉 主题设置 - 使用嵌套菜单 -->
|
<!-- 👉 主题设置 - 使用嵌套菜单 -->
|
||||||
<VMenu location="end" offset-x min-width="200" v-model="showThemeMenu" :close-on-content-click="true">
|
<VMenu location="end" offset-x min-width="200" v-model="showThemeMenu" :close-on-content-click="true">
|
||||||
<template v-slot:activator="{ props: menuProps }">
|
<template v-slot:activator="{ props: menuProps }">
|
||||||
@@ -553,9 +625,10 @@ onUnmounted(() => {
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon :icon="getThemeIcon" />
|
<VIcon :icon="getThemeIcon" />
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle>
|
<VListItemTitle>{{ t('common.theme') }}</VListItemTitle>
|
||||||
{{ themes.find(t => t.name === currentThemeName)?.title || t('common.theme') }}
|
<VListItemSubtitle>
|
||||||
</VListItemTitle>
|
{{ themes.find(t => t.name === currentThemeName)?.title || t('theme.auto') }}
|
||||||
|
</VListItemSubtitle>
|
||||||
<template #append>
|
<template #append>
|
||||||
<VIcon icon="mdi-chevron-right" size="small" />
|
<VIcon icon="mdi-chevron-right" size="small" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
saving: 'Saving',
|
saving: 'Saving',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
|
uiMode: 'UI Layout',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
pleaseWait: 'Please wait...',
|
pleaseWait: 'Please wait...',
|
||||||
viewDetails: 'View Details',
|
viewDetails: 'View Details',
|
||||||
@@ -66,6 +67,7 @@ export default {
|
|||||||
serviceUnavailable: 'Service Unavailable',
|
serviceUnavailable: 'Service Unavailable',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
preset: 'Preset',
|
preset: 'Preset',
|
||||||
|
refresh: 'Refresh',
|
||||||
newVersionAvailable: 'New version detected, please refresh the page to get the latest features',
|
newVersionAvailable: 'New version detected, please refresh the page to get the latest features',
|
||||||
},
|
},
|
||||||
mediaType: {
|
mediaType: {
|
||||||
@@ -130,6 +132,7 @@ export default {
|
|||||||
light: 'Light',
|
light: 'Light',
|
||||||
dark: 'Dark',
|
dark: 'Dark',
|
||||||
auto: 'Follow System',
|
auto: 'Follow System',
|
||||||
|
autoUI: 'Auto',
|
||||||
transparent: 'Transparent',
|
transparent: 'Transparent',
|
||||||
purple: 'Purple',
|
purple: 'Purple',
|
||||||
custom: 'Custom Style',
|
custom: 'Custom Style',
|
||||||
@@ -1226,6 +1229,7 @@ export default {
|
|||||||
title: 'About MoviePilot',
|
title: 'About MoviePilot',
|
||||||
softwareVersion: 'Software Version',
|
softwareVersion: 'Software Version',
|
||||||
frontendVersion: 'Frontend Version',
|
frontendVersion: 'Frontend Version',
|
||||||
|
browserVersion: 'Browser Cached Version',
|
||||||
authVersion: 'Auth Resource Version',
|
authVersion: 'Auth Resource Version',
|
||||||
indexerVersion: 'Indexer Resource Version',
|
indexerVersion: 'Indexer Resource Version',
|
||||||
configDir: 'Config Directory',
|
configDir: 'Config Directory',
|
||||||
@@ -1245,6 +1249,7 @@ export default {
|
|||||||
dataDirectory: '/moviepilot',
|
dataDirectory: '/moviepilot',
|
||||||
expand: 'Expand',
|
expand: 'Expand',
|
||||||
collapse: 'Collapse',
|
collapse: 'Collapse',
|
||||||
|
cleanCache: 'Clear Cache',
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
custom: 'Custom',
|
custom: 'Custom',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
saving: '保存中',
|
saving: '保存中',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
|
uiMode: '界面布局',
|
||||||
language: '语言',
|
language: '语言',
|
||||||
pleaseWait: '请稍候...',
|
pleaseWait: '请稍候...',
|
||||||
viewDetails: '查看详情',
|
viewDetails: '查看详情',
|
||||||
@@ -66,6 +67,7 @@ export default {
|
|||||||
serviceUnavailable: '服务不可用',
|
serviceUnavailable: '服务不可用',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
preset: '预设',
|
preset: '预设',
|
||||||
|
refresh: '刷新',
|
||||||
newVersionAvailable: '检测到新版本,请刷新页面以获取最新功能',
|
newVersionAvailable: '检测到新版本,请刷新页面以获取最新功能',
|
||||||
},
|
},
|
||||||
mediaType: {
|
mediaType: {
|
||||||
@@ -130,6 +132,7 @@ export default {
|
|||||||
light: '浅色',
|
light: '浅色',
|
||||||
dark: '深色',
|
dark: '深色',
|
||||||
auto: '跟随系统',
|
auto: '跟随系统',
|
||||||
|
autoUI: '自动',
|
||||||
transparent: '透明',
|
transparent: '透明',
|
||||||
purple: '幻紫',
|
purple: '幻紫',
|
||||||
custom: '附加样式',
|
custom: '附加样式',
|
||||||
@@ -1223,6 +1226,7 @@ export default {
|
|||||||
title: '关于 MoviePilot',
|
title: '关于 MoviePilot',
|
||||||
softwareVersion: '软件版本',
|
softwareVersion: '软件版本',
|
||||||
frontendVersion: '前端版本',
|
frontendVersion: '前端版本',
|
||||||
|
browserVersion: '浏览器缓存版本',
|
||||||
authVersion: '认证资源版本',
|
authVersion: '认证资源版本',
|
||||||
indexerVersion: '站点资源版本',
|
indexerVersion: '站点资源版本',
|
||||||
configDir: '配置目录',
|
configDir: '配置目录',
|
||||||
@@ -1230,7 +1234,7 @@ export default {
|
|||||||
timezone: '时区',
|
timezone: '时区',
|
||||||
latest: '最新',
|
latest: '最新',
|
||||||
supportingSites: '支持站点',
|
supportingSites: '支持站点',
|
||||||
support: '支援',
|
support: '支持',
|
||||||
documentation: '文档',
|
documentation: '文档',
|
||||||
feedback: '问题反馈',
|
feedback: '问题反馈',
|
||||||
channel: '发布频道',
|
channel: '发布频道',
|
||||||
@@ -1242,6 +1246,7 @@ export default {
|
|||||||
dataDirectory: '/moviepilot',
|
dataDirectory: '/moviepilot',
|
||||||
expand: '展开',
|
expand: '展开',
|
||||||
collapse: '收起',
|
collapse: '收起',
|
||||||
|
cleanCache: '清除缓存',
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
custom: '自定义',
|
custom: '自定义',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
saving: '保存中',
|
saving: '保存中',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
theme: '主題',
|
theme: '主題',
|
||||||
|
uiMode: '界面佈局',
|
||||||
language: '語言',
|
language: '語言',
|
||||||
pleaseWait: '請稍候...',
|
pleaseWait: '請稍候...',
|
||||||
viewDetails: '查看詳情',
|
viewDetails: '查看詳情',
|
||||||
@@ -66,6 +67,7 @@ export default {
|
|||||||
serviceUnavailable: '服務不可用',
|
serviceUnavailable: '服務不可用',
|
||||||
status: '狀態',
|
status: '狀態',
|
||||||
preset: '預設',
|
preset: '預設',
|
||||||
|
refresh: '刷新',
|
||||||
newVersionAvailable: '檢測到新版本,請刷新頁面以獲取最新功能',
|
newVersionAvailable: '檢測到新版本,請刷新頁面以獲取最新功能',
|
||||||
},
|
},
|
||||||
mediaType: {
|
mediaType: {
|
||||||
@@ -130,6 +132,7 @@ export default {
|
|||||||
light: '淺色',
|
light: '淺色',
|
||||||
dark: '深色',
|
dark: '深色',
|
||||||
auto: '跟隨系統',
|
auto: '跟隨系統',
|
||||||
|
autoUI: '自動',
|
||||||
transparent: '透明',
|
transparent: '透明',
|
||||||
purple: '幻紫',
|
purple: '幻紫',
|
||||||
custom: '附加樣式',
|
custom: '附加樣式',
|
||||||
@@ -1211,6 +1214,7 @@ export default {
|
|||||||
title: '關於 MoviePilot',
|
title: '關於 MoviePilot',
|
||||||
softwareVersion: '軟件版本',
|
softwareVersion: '軟件版本',
|
||||||
frontendVersion: '前端版本',
|
frontendVersion: '前端版本',
|
||||||
|
browserVersion: '瀏覽器緩存版本',
|
||||||
authVersion: '認證資源版本',
|
authVersion: '認證資源版本',
|
||||||
indexerVersion: '站點資源版本',
|
indexerVersion: '站點資源版本',
|
||||||
configDir: '配置目錄',
|
configDir: '配置目錄',
|
||||||
@@ -1230,6 +1234,7 @@ export default {
|
|||||||
dataDirectory: '/moviepilot',
|
dataDirectory: '/moviepilot',
|
||||||
expand: '展開',
|
expand: '展開',
|
||||||
collapse: '收起',
|
collapse: '收起',
|
||||||
|
cleanCache: '清除緩存',
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
custom: '自定義',
|
custom: '自定義',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { aliases } from 'vuetify/lib/iconsets/mdi'
|
import { aliases } from 'vuetify/iconsets/mdi'
|
||||||
|
|
||||||
const alertTypeIcon = {
|
const alertTypeIcon = {
|
||||||
success: 'mdi-check-circle-outline',
|
success: 'mdi-check-circle-outline',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { globalSettingsState } from '@/stores/types'
|
import type { globalSettingsState } from '@/stores/types'
|
||||||
import { fetchGlobalSettings } from '@/utils/globalSetting'
|
import { fetchGlobalSettings } from '@/utils/globalSetting'
|
||||||
|
import { useVersionChecker } from '@/composables/useVersionChecker'
|
||||||
|
|
||||||
export const useGlobalSettingsStore = defineStore('globalSettings', {
|
export const useGlobalSettingsStore = defineStore('globalSettings', {
|
||||||
state: (): globalSettingsState => ({
|
state: (): globalSettingsState => ({
|
||||||
@@ -18,6 +19,12 @@ export const useGlobalSettingsStore = defineStore('globalSettings', {
|
|||||||
const result = await fetchGlobalSettings()
|
const result = await fetchGlobalSettings()
|
||||||
this.data = result || {}
|
this.data = result || {}
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
|
|
||||||
|
// 检查版本更新
|
||||||
|
if (result.FRONTEND_VERSION) {
|
||||||
|
const { checkVersion } = useVersionChecker()
|
||||||
|
await checkVersion(result.FRONTEND_VERSION)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize global settings', error)
|
console.error('Failed to initialize global settings', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,58 +1,8 @@
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import i18n from '@/plugins/i18n'
|
|
||||||
|
|
||||||
// 创建一个专用的AbortController,用于globalSetting请求
|
// 创建一个专用的AbortController,用于globalSetting请求
|
||||||
const globalSettingController = new AbortController()
|
const globalSettingController = new AbortController()
|
||||||
|
|
||||||
// 声明全局变量类型
|
|
||||||
declare const __APP_VERSION__: string
|
|
||||||
|
|
||||||
// 当前前端版本号(由 Vite 在编译时注入)
|
|
||||||
const CURRENT_FRONTEND_VERSION = __APP_VERSION__
|
|
||||||
console.log(`[VersionChecker] 当前前端版本: ${CURRENT_FRONTEND_VERSION}`)
|
|
||||||
|
|
||||||
// 标记是否已经显示过版本更新通知
|
|
||||||
let versionNotificationShown = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查版本并在需要时显示更新通知
|
|
||||||
*/
|
|
||||||
async function checkVersionAndNotify(serverVersion: string): Promise<void> {
|
|
||||||
// 版本不同,且尚未显示通知
|
|
||||||
if (serverVersion !== CURRENT_FRONTEND_VERSION && !versionNotificationShown) {
|
|
||||||
versionNotificationShown = true
|
|
||||||
console.log(`[VersionChecker] 检测到版本更新: ${CURRENT_FRONTEND_VERSION} -> ${serverVersion}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 清除所有缓存
|
|
||||||
if ('caches' in window) {
|
|
||||||
const cacheNames = await caches.keys()
|
|
||||||
await Promise.all(cacheNames.map(name => caches.delete(name)))
|
|
||||||
console.log('[VersionChecker] 已清除所有缓存')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 注销Service Worker
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
||||||
await Promise.all(registrations.map(registration => registration.unregister()))
|
|
||||||
console.log('[VersionChecker] 已注销所有 Service Worker')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[VersionChecker] 清除缓存失败:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 显示持久化通知,提示用户刷新
|
|
||||||
const toast = useToast()
|
|
||||||
toast.info(i18n.global.t('common.newVersionAvailable'), {
|
|
||||||
timeout: false, // 不自动消失
|
|
||||||
closeButton: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
draggable: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchGlobalSettings() {
|
export async function fetchGlobalSettings() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/global', {
|
const result: { [key: string]: any } = await api.get('system/global', {
|
||||||
@@ -62,15 +12,7 @@ export async function fetchGlobalSettings() {
|
|||||||
// 手动设置signal,防止reqestOptimizer添加可中断的controller
|
// 手动设置signal,防止reqestOptimizer添加可中断的controller
|
||||||
signal: globalSettingController.signal,
|
signal: globalSettingController.signal,
|
||||||
})
|
})
|
||||||
|
return result.data || {}
|
||||||
const data = result.data || {}
|
|
||||||
|
|
||||||
// 检查版本更新
|
|
||||||
if (data.FRONTEND_VERSION) {
|
|
||||||
await checkVersionAndNotify(data.FRONTEND_VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch global settings', error)
|
console.error('Failed to fetch global settings', error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ export default defineConfig({
|
|||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
api: 'modern-compiler',
|
||||||
quietDeps: true,
|
quietDeps: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3017,9 +3017,9 @@ camelcase-css@^2.0.1:
|
|||||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
|
caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
|
||||||
version "1.0.30001715"
|
version "1.0.30001761"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz"
|
||||||
integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==
|
integrity sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==
|
||||||
|
|
||||||
chalk@^4.0.0, chalk@^4.0.2:
|
chalk@^4.0.0, chalk@^4.0.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
@@ -6856,6 +6856,7 @@ std-env@^3.9.0:
|
|||||||
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
|
||||||
|
name string-width-cjs
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -6941,6 +6942,7 @@ stringify-object@^3.3.0:
|
|||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
|
name strip-ansi-cjs
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
|||||||
Reference in New Issue
Block a user