From b3fc9a231ec5e1713e5a45a0b7a73bf86b9190c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E6=B0=B4=E6=B8=85?= Date: Thu, 4 Sep 2025 17:29:36 +0800 Subject: [PATCH 1/3] add url filter --- .../DownloadList/OptionSection/index.js | 19 ++++- .../DownloadList/OptionSection/styles.js | 32 +++++++++ src/devtoolApp/hooks/useAppSaveAllResource.js | 15 ++-- src/devtoolApp/store/option/index.js | 13 ++++ src/devtoolApp/utils/file.js | 2 +- src/devtoolApp/utils/general.js | 69 +++++++++++++++++++ 6 files changed, 142 insertions(+), 8 deletions(-) diff --git a/src/devtoolApp/components/DownloadList/OptionSection/index.js b/src/devtoolApp/components/DownloadList/OptionSection/index.js index d652fcd..578ccef 100644 --- a/src/devtoolApp/components/DownloadList/OptionSection/index.js +++ b/src/devtoolApp/components/DownloadList/OptionSection/index.js @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { Toggle } from '../../Toggle'; -import { OptionSectionWrapper } from './styles'; +import { OptionSectionWrapper, FilterInputWrapper, FilterInput, FilterLabel } from './styles'; import * as optionActions from 'devtoolApp/store/option'; import useStore from 'devtoolApp/store'; @@ -8,7 +8,7 @@ export const OptionSection = () => { const { dispatch, state: { - option: { ignoreNoContentFile, beautifyFile }, + option: { ignoreNoContentFile, beautifyFile, urlFilter }, ui: { isSaving }, }, } = useStore(); @@ -21,8 +21,23 @@ export const OptionSection = () => { dispatch(optionActions.setBeautifyFile(willBeautify)); }, []); + const handleUrlFilterChange = useCallback((e) => { + dispatch(optionActions.setUrlFilter(e.target.value)); + }, []); + return ( + + URL Filter (glob patterns, separate multiple rules with |): + + Ignore "No Content" files diff --git a/src/devtoolApp/components/DownloadList/OptionSection/styles.js b/src/devtoolApp/components/DownloadList/OptionSection/styles.js index a495807..5f662bf 100644 --- a/src/devtoolApp/components/DownloadList/OptionSection/styles.js +++ b/src/devtoolApp/components/DownloadList/OptionSection/styles.js @@ -3,3 +3,35 @@ import styled from 'styled-components'; export const OptionSectionWrapper = styled.div` padding: 0 20px 20px 20px; `; + +export const FilterInputWrapper = styled.div` + margin-bottom: 10px; +`; + +export const FilterInput = styled.input` + width: 100%; + padding: 8px 12px; + border: 1px solid ${({ theme }) => theme.grayScale.gray5}; + border-radius: 4px; + background-color: ${({ theme }) => theme.background}; + color: ${({ theme }) => theme.text}; + font-size: 14px; + + &:focus { + outline: none; + border-color: ${({ theme }) => theme.primary}; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +`; + +export const FilterLabel = styled.label` + display: block; + margin-bottom: 4px; + font-size: 12px; + color: ${({ theme }) => theme.grayScale.gray10}; + font-weight: 500; +`; diff --git a/src/devtoolApp/hooks/useAppSaveAllResource.js b/src/devtoolApp/hooks/useAppSaveAllResource.js index 28285e3..451839f 100644 --- a/src/devtoolApp/hooks/useAppSaveAllResource.js +++ b/src/devtoolApp/hooks/useAppSaveAllResource.js @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'; import * as uiActions from '../store/ui'; import { downloadZipFile, resolveDuplicatedResources } from '../utils/file'; import { logResourceByUrl } from '../utils/resource'; +import { globMatch } from '../utils/general'; import { resetNetworkResource } from '../store/networkResource'; import { resetStaticResource } from '../store/staticResource'; import { INITIAL_STATE as UI_INITIAL_STATE } from '../store/ui'; @@ -14,7 +15,7 @@ export const useAppSaveAllResource = () => { const staticResourceRef = useRef(staticResource); const { downloadList, - option: { ignoreNoContentFile, beautifyFile }, + option: { ignoreNoContentFile, beautifyFile, urlFilter }, ui: { tab }, } = state; @@ -54,16 +55,20 @@ export const useAppSaveAllResource = () => { ...(networkResourceRef.current || []), ...(staticResourceRef.current || []), ]); - console.log(toDownload.filter(t => typeof t?.content !== 'string' && !!t?.content?.then)); - if (loaded && toDownload.length) { + + // Apply URL filter if specified + const filteredToDownload = urlFilter ? toDownload.filter((resource) => globMatch(urlFilter, resource.url)) : toDownload; + + console.log(toDownload.filter((t) => typeof t?.content !== 'string' && !!t?.content?.then)); + if (loaded && filteredToDownload.length) { downloadZipFile( - toDownload, + filteredToDownload, { ignoreNoContentFile, beautifyFile }, (item, isDone) => { dispatch(uiActions.setStatus(`Compressed: ${item.url} Processed: ${isDone}`)); }, () => { - logResourceByUrl(dispatch, downloadItem.url, toDownload); + logResourceByUrl(dispatch, downloadItem.url, filteredToDownload); if (i + 1 !== downloadList.length) { dispatch(resetNetworkResource()); dispatch(resetStaticResource()); diff --git a/src/devtoolApp/store/option/index.js b/src/devtoolApp/store/option/index.js index ee7aff7..8efb83f 100644 --- a/src/devtoolApp/store/option/index.js +++ b/src/devtoolApp/store/option/index.js @@ -5,11 +5,13 @@ export const STATE_KEY = `option`; export const ACTIONS = { SET_IGNORE_NO_CONTENT_FILE: 'SET_IGNORE_NO_CONTENT_FILE', SET_BEAUTIFY_FILE: 'SET_BEAUTIFY_FILE', + SET_URL_FILTER: 'SET_URL_FILTER', }; export const INITIAL_STATE = { ignoreNoContentFile: false, beautifyFile: false, + urlFilter: '', }; export const setIgnoreNoContentFile = (willIgnore) => ({ @@ -22,6 +24,11 @@ export const setBeautifyFile = (willBeautify) => ({ payload: !!willBeautify, }); +export const setUrlFilter = (filter) => ({ + type: ACTIONS.SET_URL_FILTER, + payload: filter || '', +}); + export const uiReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case ACTIONS.SET_IGNORE_NO_CONTENT_FILE: { @@ -36,6 +43,12 @@ export const uiReducer = (state = INITIAL_STATE, action) => { beautifyFile: action.payload, }; } + case ACTIONS.SET_URL_FILTER: { + return { + ...state, + urlFilter: action.payload, + }; + } default: { return state; } diff --git a/src/devtoolApp/utils/file.js b/src/devtoolApp/utils/file.js index b19fb00..3aac8a4 100644 --- a/src/devtoolApp/utils/file.js +++ b/src/devtoolApp/utils/file.js @@ -73,7 +73,7 @@ export const getContentRead = (item) => { export const addItemsToZipWriter = (zipWriter, items, options, eachDoneCallback, callback) => { const item = items[0]; const rest = items.slice(1); - + // console.log(item); // if item exist so add it to zip if (item) { // Beautify here diff --git a/src/devtoolApp/utils/general.js b/src/devtoolApp/utils/general.js index 78f8f6b..5b950d6 100644 --- a/src/devtoolApp/utils/general.js +++ b/src/devtoolApp/utils/general.js @@ -25,3 +25,72 @@ export const logIfDev = (...props) => { console.log('[DEVTOOL]', ...props); } }; + +/** + * Simple glob pattern matching + * Supports * (wildcard) and ** (recursive wildcard) + * Supports multiple patterns separated by | (pipe) + * @param {string} pattern - glob pattern(s), can be separated by | + * @param {string} str - string to match + * @returns {boolean} - whether the string matches the pattern + */ +export const globMatch = (pattern, str) => { + if (!pattern || pattern.trim() === '') return true; // Empty pattern matches everything + if (!str) return false; + + // Split pattern by | to support multiple patterns + const patterns = pattern.split('|').map(p => p.trim()).filter(p => p); + + // If no valid patterns after filtering, match everything + if (patterns.length === 0) return true; + + // If any pattern matches, return true + return patterns.some(singlePattern => { + // Convert single glob pattern to regex pattern + let regexPattern = ''; + let i = 0; + + while (i < singlePattern.length) { + const char = singlePattern[i]; + + if (char === '*') { + if (i + 1 < singlePattern.length && singlePattern[i + 1] === '*') { + // Handle ** (match any number of directories) + if (i + 2 < singlePattern.length && singlePattern[i + 2] === '/') { + regexPattern += '(.*\/)?'; + i += 3; // Skip **/ + } else { + regexPattern += '.*'; + i += 2; // Skip ** + } + } else { + // Handle single * (match any characters) + regexPattern += '.*'; + i++; + } + } else if (char === '?') { + regexPattern += '.'; + i++; + } else { + // Escape special regex characters + if (/[.+^${}()|[\]\\]/.test(char)) { + regexPattern += '\\' + char; + } else { + regexPattern += char; + } + i++; + } + } + + // Anchor the pattern + regexPattern = '^' + regexPattern + '$'; + + try { + const regex = new RegExp(regexPattern, 'i'); // Case insensitive + return regex.test(str); + } catch (e) { + console.warn('[DEVTOOL] Invalid glob pattern:', singlePattern, e); + return false; + } + }); +}; From 3f92bbffb02c5276ecdcf31e402607152eb31fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E6=B0=B4=E6=B8=85?= Date: Thu, 4 Sep 2025 17:51:43 +0800 Subject: [PATCH 2/3] use minimatch for glob, save filter to sessionStorage --- package.json | 1 + .../DownloadList/OptionSection/index.js | 55 +++++++++- src/devtoolApp/store/option/index.js | 24 ++++- src/devtoolApp/utils/general.js | 102 ++++++++++-------- 4 files changed, 130 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index f55d580..0446865 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "context": "^3.0.4", "global": "^4.4.0", "lodash": "^4.17.21", + "minimatch": "^10.0.3", "polished": "^4.2.2", "prettier": "^2.7.1", "react": "^18.2.0", diff --git a/src/devtoolApp/components/DownloadList/OptionSection/index.js b/src/devtoolApp/components/DownloadList/OptionSection/index.js index 578ccef..859c643 100644 --- a/src/devtoolApp/components/DownloadList/OptionSection/index.js +++ b/src/devtoolApp/components/DownloadList/OptionSection/index.js @@ -28,13 +28,64 @@ export const OptionSection = () => { return ( - URL Filter (glob patterns, separate multiple rules with |): + URL Filter (supports glob patterns with negation, separate multiple rules with |): + + + Ignore "No Content" files + + + Beautify HTML, CSS, JS, JSON files + + + ); +}; + +export default OptionSection; +import React, { useCallback } from 'react'; +import { Toggle } from '../../Toggle'; +import { OptionSectionWrapper, FilterInputWrapper, FilterInput, FilterLabel } from './styles'; +import * as optionActions from 'devtoolApp/store/option'; +import useStore from 'devtoolApp/store'; + +export const OptionSection = () => { + const { + dispatch, + state: { + option: { ignoreNoContentFile, beautifyFile, urlFilter }, + ui: { isSaving }, + }, + } = useStore(); + + const handleIgnoreNoContentFile = useCallback((willIgnore) => { + dispatch(optionActions.setIgnoreNoContentFile(willIgnore)); + }, []); + + const handleBeautifyFile = useCallback((willBeautify) => { + dispatch(optionActions.setBeautifyFile(willBeautify)); + }, []); + + const handleUrlFilterChange = useCallback((e) => { + dispatch(optionActions.setUrlFilter(e.target.value)); + }, []); + + return ( + + + URL Filter (supports glob patterns with negation, separate multiple rules with |): + diff --git a/src/devtoolApp/store/option/index.js b/src/devtoolApp/store/option/index.js index 8efb83f..824d9a4 100644 --- a/src/devtoolApp/store/option/index.js +++ b/src/devtoolApp/store/option/index.js @@ -1,4 +1,5 @@ import { getReducerConfig } from '../utils'; +import { loadUrlFilterFromStorage, saveUrlFilterToStorage } from 'devtoolApp/utils/general'; export const STATE_KEY = `option`; @@ -6,12 +7,13 @@ export const ACTIONS = { SET_IGNORE_NO_CONTENT_FILE: 'SET_IGNORE_NO_CONTENT_FILE', SET_BEAUTIFY_FILE: 'SET_BEAUTIFY_FILE', SET_URL_FILTER: 'SET_URL_FILTER', + LOAD_URL_FILTER_FROM_STORAGE: 'LOAD_URL_FILTER_FROM_STORAGE', }; export const INITIAL_STATE = { ignoreNoContentFile: false, beautifyFile: false, - urlFilter: '', + urlFilter: loadUrlFilterFromStorage(), // Load from sessionStorage on initialization }; export const setIgnoreNoContentFile = (willIgnore) => ({ @@ -24,9 +26,20 @@ export const setBeautifyFile = (willBeautify) => ({ payload: !!willBeautify, }); -export const setUrlFilter = (filter) => ({ - type: ACTIONS.SET_URL_FILTER, - payload: filter || '', +export const setUrlFilter = (filter) => { + const urlFilter = filter || ''; + // Save to sessionStorage when setting URL filter + saveUrlFilterToStorage(urlFilter); + + return { + type: ACTIONS.SET_URL_FILTER, + payload: urlFilter, + }; +}; + +export const loadUrlFilterFromStorageAction = () => ({ + type: ACTIONS.LOAD_URL_FILTER_FROM_STORAGE, + payload: loadUrlFilterFromStorage(), }); export const uiReducer = (state = INITIAL_STATE, action) => { @@ -43,7 +56,8 @@ export const uiReducer = (state = INITIAL_STATE, action) => { beautifyFile: action.payload, }; } - case ACTIONS.SET_URL_FILTER: { + case ACTIONS.SET_URL_FILTER: + case ACTIONS.LOAD_URL_FILTER_FROM_STORAGE: { return { ...state, urlFilter: action.payload, diff --git a/src/devtoolApp/utils/general.js b/src/devtoolApp/utils/general.js index 5b950d6..eade701 100644 --- a/src/devtoolApp/utils/general.js +++ b/src/devtoolApp/utils/general.js @@ -26,9 +26,11 @@ export const logIfDev = (...props) => { } }; +import { minimatch } from 'minimatch'; + /** - * Simple glob pattern matching - * Supports * (wildcard) and ** (recursive wildcard) + * Glob pattern matching using minimatch library + * Supports * (wildcard), ** (recursive wildcard), and negation with ! * Supports multiple patterns separated by | (pipe) * @param {string} pattern - glob pattern(s), can be separated by | * @param {string} str - string to match @@ -44,53 +46,63 @@ export const globMatch = (pattern, str) => { // If no valid patterns after filtering, match everything if (patterns.length === 0) return true; - // If any pattern matches, return true - return patterns.some(singlePattern => { - // Convert single glob pattern to regex pattern - let regexPattern = ''; - let i = 0; - - while (i < singlePattern.length) { - const char = singlePattern[i]; - - if (char === '*') { - if (i + 1 < singlePattern.length && singlePattern[i + 1] === '*') { - // Handle ** (match any number of directories) - if (i + 2 < singlePattern.length && singlePattern[i + 2] === '/') { - regexPattern += '(.*\/)?'; - i += 3; // Skip **/ - } else { - regexPattern += '.*'; - i += 2; // Skip ** - } - } else { - // Handle single * (match any characters) - regexPattern += '.*'; - i++; - } - } else if (char === '?') { - regexPattern += '.'; - i++; - } else { - // Escape special regex characters - if (/[.+^${}()|[\]\\]/.test(char)) { - regexPattern += '\\' + char; - } else { - regexPattern += char; - } - i++; - } + // Separate positive and negative patterns + const positivePatterns = []; + const negativePatterns = []; + + patterns.forEach(singlePattern => { + if (singlePattern.startsWith('!')) { + negativePatterns.push(singlePattern.slice(1)); // Remove the ! prefix + } else { + positivePatterns.push(singlePattern); } - - // Anchor the pattern - regexPattern = '^' + regexPattern + '$'; - + }); + + // If there are no positive patterns, default to match all + const hasPositiveMatch = positivePatterns.length === 0 || positivePatterns.some(pattern => { + try { + return minimatch(str, pattern, { nocase: true }); + } catch (e) { + console.warn('[DEVTOOL] Invalid glob pattern:', pattern, e); + return false; + } + }); + + // If positive patterns don't match, return false + if (!hasPositiveMatch) return false; + + // Check negative patterns - if any negative pattern matches, exclude this item + const hasNegativeMatch = negativePatterns.some(pattern => { try { - const regex = new RegExp(regexPattern, 'i'); // Case insensitive - return regex.test(str); + return minimatch(str, pattern, { nocase: true }); } catch (e) { - console.warn('[DEVTOOL] Invalid glob pattern:', singlePattern, e); + console.warn('[DEVTOOL] Invalid negative glob pattern:', pattern, e); return false; } }); + + // Return true if positive matches and no negative matches + return !hasNegativeMatch; +}; + +/** + * Storage utilities for URL filter persistence + */ +const URL_FILTER_STORAGE_KEY = 'resources_saver_url_filter'; + +export const saveUrlFilterToStorage = (filter) => { + try { + sessionStorage.setItem(URL_FILTER_STORAGE_KEY, filter || ''); + } catch (e) { + console.warn('[DEVTOOL] Failed to save URL filter to sessionStorage:', e); + } +}; + +export const loadUrlFilterFromStorage = () => { + try { + return sessionStorage.getItem(URL_FILTER_STORAGE_KEY) || ''; + } catch (e) { + console.warn('[DEVTOOL] Failed to load URL filter from sessionStorage:', e); + return ''; + } }; From cbfd4e53c1bf8371fc68a19c1066ed07cf22a330 Mon Sep 17 00:00:00 2001 From: ksky521 Date: Thu, 4 Sep 2025 23:03:03 +0800 Subject: [PATCH 3/3] typo --- .../DownloadList/OptionSection/index.js | 55 +------------------ 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/src/devtoolApp/components/DownloadList/OptionSection/index.js b/src/devtoolApp/components/DownloadList/OptionSection/index.js index 859c643..34074ac 100644 --- a/src/devtoolApp/components/DownloadList/OptionSection/index.js +++ b/src/devtoolApp/components/DownloadList/OptionSection/index.js @@ -28,58 +28,9 @@ export const OptionSection = () => { return ( - URL Filter (supports glob patterns with negation, separate multiple rules with |): - - - - Ignore "No Content" files - - - Beautify HTML, CSS, JS, JSON files - - - ); -}; - -export default OptionSection; -import React, { useCallback } from 'react'; -import { Toggle } from '../../Toggle'; -import { OptionSectionWrapper, FilterInputWrapper, FilterInput, FilterLabel } from './styles'; -import * as optionActions from 'devtoolApp/store/option'; -import useStore from 'devtoolApp/store'; - -export const OptionSection = () => { - const { - dispatch, - state: { - option: { ignoreNoContentFile, beautifyFile, urlFilter }, - ui: { isSaving }, - }, - } = useStore(); - - const handleIgnoreNoContentFile = useCallback((willIgnore) => { - dispatch(optionActions.setIgnoreNoContentFile(willIgnore)); - }, []); - - const handleBeautifyFile = useCallback((willBeautify) => { - dispatch(optionActions.setBeautifyFile(willBeautify)); - }, []); - - const handleUrlFilterChange = useCallback((e) => { - dispatch(optionActions.setUrlFilter(e.target.value)); - }, []); - - return ( - - - URL Filter (supports glob patterns with negation, separate multiple rules with |): + + URL Filter (supports glob patterns with negation, separate multiple rules with |): +