diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-podkop/src/helpers/index.ts index 43c8879..da7da80 100644 --- a/fe-app-podkop/src/helpers/index.ts +++ b/fe-app-podkop/src/helpers/index.ts @@ -8,3 +8,4 @@ export * from './getProxyUrlName'; export * from './onMount'; export * from './getClashApiUrl'; export * from './splitProxyString'; +export * from './preserveScrollForPage'; diff --git a/fe-app-podkop/src/helpers/preserveScrollForPage.ts b/fe-app-podkop/src/helpers/preserveScrollForPage.ts new file mode 100644 index 0000000..c56eb8d --- /dev/null +++ b/fe-app-podkop/src/helpers/preserveScrollForPage.ts @@ -0,0 +1,9 @@ +export function preserveScrollForPage(renderFn: () => void) { + const scrollY = window.scrollY; + + renderFn(); + + requestAnimationFrame(() => { + window.scrollTo({ top: scrollY }); + }); +} diff --git a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts index e9244e9..28a5573 100644 --- a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts @@ -62,9 +62,10 @@ export async function getDashboardSections(): Promise { - const newDiv = document.createElement('div'); - newDiv.className = 'skeleton'; - newDiv.style.width = '99px'; - newDiv.style.height = '28px'; - el.replaceWith(newDiv); - }); + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: false, + }, + }); } // Renderer @@ -170,8 +191,12 @@ async function renderSectionsWidget() { }, onTestLatency: () => {}, onChooseOutbound: () => {}, + latencyFetching: sectionsWidget.latencyFetching, + }); + + return preserveScrollForPage(() => { + container!.replaceChildren(renderedWidget); }); - return container!.replaceChildren(renderedWidget); } const renderedWidgets = sectionsWidget.data.map((section) => @@ -179,9 +204,8 @@ async function renderSectionsWidget() { loading: sectionsWidget.loading, failed: sectionsWidget.failed, section, + latencyFetching: sectionsWidget.latencyFetching, onTestLatency: (tag) => { - replaceTestLatencyButtonsWithSkeleton(); - if (section.withTagSelect) { return handleTestGroupLatency(tag); } @@ -194,7 +218,9 @@ async function renderSectionsWidget() { }), ); - return container!.replaceChildren(...renderedWidgets); + return preserveScrollForPage(() => { + container!.replaceChildren(...renderedWidgets); + }); } async function renderBandwidthWidget() { diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts index 1acdb26..b302ba7 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts @@ -6,6 +6,7 @@ interface IRenderSectionsProps { section: Podkop.OutboundGroup; onTestLatency: (tag: string) => void; onChooseOutbound: (selector: string, tag: string) => void; + latencyFetching: boolean; } function renderFailedState() { @@ -31,6 +32,7 @@ export function renderDefaultState({ section, onChooseOutbound, onTestLatency, + latencyFetching, }: IRenderSectionsProps) { function testLatency() { if (section.withTagSelect) { @@ -95,14 +97,16 @@ export function renderDefaultState({ }, section.displayName, ), - E( - 'button', - { - class: 'btn dashboard-sections-grid-item-test-latency', - click: () => testLatency(), - }, - _('Test latency'), - ), + latencyFetching + ? E('div', { class: 'skeleton', style: 'width: 99px; height: 28px' }) + : E( + 'button', + { + class: 'btn dashboard-sections-grid-item-test-latency', + click: () => testLatency(), + }, + _('Test latency'), + ), ]), E( 'div', diff --git a/fe-app-podkop/src/store.ts b/fe-app-podkop/src/store.ts index 4f5f4e8..4591353 100644 --- a/fe-app-podkop/src/store.ts +++ b/fe-app-podkop/src/store.ts @@ -141,6 +141,7 @@ export interface StoreType { loading: boolean; failed: boolean; data: Podkop.OutboundGroup[]; + latencyFetching: boolean; }; } @@ -172,6 +173,7 @@ const initialStore: StoreType = { sectionsWidget: { loading: true, failed: false, + latencyFetching: false, data: [], }, }; diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index e983be7..41331df 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -776,6 +776,15 @@ function splitProxyString(str) { return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean); } +// src/helpers/preserveScrollForPage.ts +function preserveScrollForPage(renderFn) { + const scrollY = window.scrollY; + renderFn(); + requestAnimationFrame(() => { + window.scrollTo({ top: scrollY }); + }); +} + // src/clash/methods/createBaseApiRequest.ts async function createBaseApiRequest(fetchFn) { try { @@ -1210,6 +1219,7 @@ var initialStore = { sectionsWidget: { loading: true, failed: false, + latencyFetching: false, data: [] } }; @@ -1248,7 +1258,8 @@ function renderLoadingState() { function renderDefaultState({ section, onChooseOutbound, - onTestLatency + onTestLatency, + latencyFetching }) { function testLatency() { if (section.withTagSelect) { @@ -1304,7 +1315,7 @@ function renderDefaultState({ }, section.displayName ), - E( + latencyFetching ? E("div", { class: "skeleton", style: "width: 99px; height: 28px" }) : E( "button", { class: "btn dashboard-sections-grid-item-test-latency", @@ -1573,6 +1584,7 @@ async function fetchDashboardSections() { const { data, success } = await getDashboardSections(); store.set({ sectionsWidget: { + latencyFetching: false, loading: false, failed: !success, data @@ -1662,20 +1674,35 @@ async function handleChooseOutbound(selector, tag) { await fetchDashboardSections(); } async function handleTestGroupLatency(tag) { + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: true + } + }); await triggerLatencyGroupTest(tag); await fetchDashboardSections(); + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: false + } + }); } async function handleTestProxyLatency(tag) { + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: true + } + }); await triggerLatencyProxyTest(tag); await fetchDashboardSections(); -} -function replaceTestLatencyButtonsWithSkeleton() { - document.querySelectorAll(".dashboard-sections-grid-item-test-latency").forEach((el) => { - const newDiv = document.createElement("div"); - newDiv.className = "skeleton"; - newDiv.style.width = "99px"; - newDiv.style.height = "28px"; - el.replaceWith(newDiv); + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: false + } }); } async function renderSectionsWidget() { @@ -1695,17 +1722,20 @@ async function renderSectionsWidget() { onTestLatency: () => { }, onChooseOutbound: () => { - } + }, + latencyFetching: sectionsWidget.latencyFetching + }); + return preserveScrollForPage(() => { + container.replaceChildren(renderedWidget); }); - return container.replaceChildren(renderedWidget); } const renderedWidgets = sectionsWidget.data.map( (section) => renderSections({ loading: sectionsWidget.loading, failed: sectionsWidget.failed, section, + latencyFetching: sectionsWidget.latencyFetching, onTestLatency: (tag) => { - replaceTestLatencyButtonsWithSkeleton(); if (section.withTagSelect) { return handleTestGroupLatency(tag); } @@ -1716,7 +1746,9 @@ async function renderSectionsWidget() { } }) ); - return container.replaceChildren(...renderedWidgets); + return preserveScrollForPage(() => { + container.replaceChildren(...renderedWidgets); + }); } async function renderBandwidthWidget() { console.log("renderBandwidthWidget"); @@ -1906,6 +1938,7 @@ return baseclass.extend({ maskIP, onMount, parseValueList, + preserveScrollForPage, renderDashboard, splitProxyString, triggerLatencyGroupTest,