1 package edu.jiangxin.apktoolbox.pdf.stat;
2
3 import edu.jiangxin.apktoolbox.pdf.PdfUtils;
4 import edu.jiangxin.apktoolbox.swing.extend.EasyPanel;
5 import edu.jiangxin.apktoolbox.swing.extend.FileListPanel;
6 import edu.jiangxin.apktoolbox.utils.Constants;
7 import edu.jiangxin.apktoolbox.utils.DateUtils;
8 import edu.jiangxin.apktoolbox.utils.ExcelExporter;
9 import edu.jiangxin.apktoolbox.utils.FileUtils;
10
11 import javax.swing.*;
12 import javax.swing.table.DefaultTableModel;
13 import java.awt.*;
14 import java.awt.event.ActionEvent;
15 import java.awt.event.ActionListener;
16 import java.awt.event.MouseAdapter;
17 import java.awt.event.MouseEvent;
18 import java.io.File;
19 import java.io.Serial;
20 import java.util.*;
21 import java.util.List;
22 import java.util.concurrent.CopyOnWriteArrayList;
23 import java.util.concurrent.ExecutorService;
24 import java.util.concurrent.Executors;
25 import java.util.concurrent.Future;
26 import java.util.concurrent.atomic.AtomicInteger;
27 import java.util.concurrent.atomic.AtomicLong;
28
29 public class PdfStatPanel extends EasyPanel {
30
31 @Serial
32 private static final long serialVersionUID = 1L;
33
34 private JTabbedPane tabbedPane;
35
36 private JPanel mainPanel;
37
38 private FileListPanel fileListPanel;
39
40 private JRadioButton pageCountRadioButton;
41
42 private JCheckBox isRecursiveSearched;
43
44 private JPanel resultPanel;
45
46 private JTable resultTable;
47
48 private DefaultTableModel resultTableModel;
49
50 private JLabel statInfoLabel;
51
52 private JButton statButton;
53 private JButton cancelButton;
54
55 private JProgressBar progressBar;
56
57 private transient SearchThread searchThread;
58
59 private final AtomicLong totalFileSize = new AtomicLong(0);
60
61 private final AtomicInteger totalPageCount = new AtomicInteger(0);
62
63 private transient final List<Vector<Object>> resultFileList = new CopyOnWriteArrayList<>();
64
65
66 @Override
67 public void initUI() {
68 tabbedPane = new JTabbedPane();
69 add(tabbedPane);
70
71 createMainPanel();
72 tabbedPane.addTab("Option", null, mainPanel, "Show Stat Options");
73
74 createResultPanel();
75 tabbedPane.addTab("Result", null, resultPanel, "Show Stat Result");
76 }
77
78 private void createMainPanel() {
79 mainPanel = new JPanel();
80 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
81
82 fileListPanel = new FileListPanel();
83 fileListPanel.initialize();
84
85 JPanel checkOptionPanel = new JPanel();
86 checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
87 checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));
88
89 ButtonGroup buttonGroup = new ButtonGroup();
90
91 pageCountRadioButton = new JRadioButton("统计页数");
92 pageCountRadioButton.setSelected(true);
93 pageCountRadioButton.setEnabled(false);
94 buttonGroup.add(pageCountRadioButton);
95
96 JPanel typePanel = new JPanel();
97 typePanel.setLayout(new FlowLayout(FlowLayout.LEFT,10,3));
98 typePanel.add(pageCountRadioButton);
99
100 checkOptionPanel.add(typePanel);
101 checkOptionPanel.add(Box.createHorizontalGlue());
102
103 JPanel searchOptionPanel = new JPanel();
104 searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
105 searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Stat Options"));
106
107 isRecursiveSearched = new JCheckBox("Recursive");
108 isRecursiveSearched.setSelected(true);
109 searchOptionPanel.add(isRecursiveSearched);
110 searchOptionPanel.add(Box.createHorizontalGlue());
111
112 JPanel operationPanel = new JPanel();
113 operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
114 operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));
115
116 JPanel buttonPanel = new JPanel();
117 buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
118
119 statButton = new JButton("Stat");
120 cancelButton = new JButton("Cancel");
121 cancelButton.setEnabled(false);
122 statButton.addActionListener(new OperationButtonActionListener());
123 cancelButton.addActionListener(new OperationButtonActionListener());
124 operationPanel.add(statButton);
125 operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
126 operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
127 operationPanel.add(cancelButton);
128 operationPanel.add(Box.createHorizontalGlue());
129
130 progressBar = new JProgressBar();
131 progressBar.setStringPainted(true);
132 progressBar.setString("Ready");
133
134 mainPanel.add(fileListPanel);
135 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
136 mainPanel.add(checkOptionPanel);
137 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
138 mainPanel.add(searchOptionPanel);
139 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
140 mainPanel.add(operationPanel);
141 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
142 mainPanel.add(progressBar);
143 }
144
145 private void createResultPanel() {
146 resultPanel = new JPanel();
147 resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));
148
149 resultTableModel = new PdfFilesTableModel(new Vector<>(), PdfFilesConstants.COLUMN_NAMES);
150 resultTable = new JTable(resultTableModel);
151 resultTable.setDefaultRenderer(Vector.class, new PdfFilesTableCellRenderer());
152 for (int i = 0; i < resultTable.getColumnCount(); i++) {
153 resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new PdfFilesTableCellRenderer());
154 }
155 resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
156 resultTable.addMouseListener(new MouseAdapter() {
157 @Override
158 public void mouseReleased(MouseEvent e) {
159 if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
160 JPopupMenu popupmenu = new JPopupMenu();
161 JMenuItem exportMenuItem = new JMenuItem("导出到 Excel");
162 exportMenuItem.addActionListener(ev ->
163 ExcelExporter.export(resultTableModel, "pdf_stat_export.xlsx", PdfStatPanel.this));
164 popupmenu.add(exportMenuItem);
165 popupmenu.show(e.getComponent(), e.getX(), e.getY());
166 }
167 }
168 });
169 JScrollPane scrollPane = new JScrollPane(resultTable);
170
171 JPanel statInfoPanel = new JPanel();
172 statInfoPanel.setLayout(new BoxLayout(statInfoPanel, BoxLayout.X_AXIS));
173 statInfoLabel = new JLabel("");
174 statInfoPanel.add(statInfoLabel);
175 statInfoPanel.add(Box.createHorizontalGlue());
176
177 resultPanel.add(scrollPane);
178 resultPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
179 resultPanel.add(statInfoPanel);
180 }
181
182 private void processFile(File file) {
183 if (pageCountRadioButton.isSelected()) {
184 Vector<Object> fileVector = getRowVector(file);
185 resultFileList.add(fileVector);
186 } else {
187 logger.error("Invalid option selected");
188 }
189 }
190
191 class OperationButtonActionListener implements ActionListener {
192 @Override
193 public void actionPerformed(ActionEvent e) {
194 Object source = e.getSource();
195 if (source.equals(statButton)) {
196 statButton.setEnabled(false);
197 cancelButton.setEnabled(true);
198 searchThread = new SearchThread(isRecursiveSearched.isSelected());
199 searchThread.start();
200 } else if (source.equals(cancelButton)) {
201 statButton.setEnabled(true);
202 cancelButton.setEnabled(false);
203 if (searchThread.isAlive()) {
204 searchThread.interrupt();
205 searchThread.executorService.shutdownNow();
206 }
207 }
208
209 }
210 }
211
212 private void showResult() {
213 SwingUtilities.invokeLater(() -> {
214 int index = 0;
215 for (Vector<Object> file : resultFileList) {
216 file.add(0, ++index);
217 resultTableModel.addRow(file);
218 }
219 tabbedPane.setSelectedIndex(1);
220 statInfoLabel.setText("Page Count: " + totalPageCount.get() + ", Total Size: " + FileUtils.sizeOfInHumanFormat(totalFileSize.get()));
221 });
222 }
223
224 private Vector<Object> getRowVector(File file) {
225 Vector<Object> rowData = new Vector<>();
226 rowData.add(file.getParent());
227 rowData.add(file.getName());
228 totalFileSize.addAndGet(file.length());
229 rowData.add(FileUtils.sizeOfInHumanFormat(file));
230 rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
231 int pageCount = PdfUtils.getPageCount(file);
232 totalPageCount.addAndGet(pageCount);
233 rowData.add(pageCount);
234 return rowData;
235 }
236
237 class SearchThread extends Thread {
238 public final ExecutorService executorService;
239 private final AtomicInteger processedFiles = new AtomicInteger(0);
240 private int totalFiles = 0;
241 private final boolean isRecursiveSearched;
242
243 public SearchThread(boolean isRecursiveSearched) {
244 super();
245 this.isRecursiveSearched = isRecursiveSearched;
246 this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
247
248 resultFileList.clear();
249 totalFileSize.set(0);
250 totalPageCount.set(0);
251
252 SwingUtilities.invokeLater(() -> {
253 progressBar.setValue(0);
254 progressBar.setString("Starting search...");
255 resultTableModel.setRowCount(0);
256 statInfoLabel.setText("");
257 });
258 }
259
260 @Override
261 public void run() {
262 try {
263 List<File> fileList = fileListPanel.getFileList();
264 Set<File> fileSet = new TreeSet<>();
265 String[] extensions = new String[]{"pdf", "PDF"};
266 for (File file : fileList) {
267 fileSet.addAll(FileUtils.listFiles(file, extensions, isRecursiveSearched));
268 }
269
270 List<Future<?>> futures = new ArrayList<>();
271 totalFiles = fileSet.size();
272 updateProgress();
273
274 for (File file : fileSet) {
275 if (currentThread().isInterrupted()) {
276 return;
277 }
278 futures.add(executorService.submit(() -> {
279 if (currentThread().isInterrupted()) {
280 return null;
281 }
282 processFile(file);
283 incrementProcessedFiles();
284 return null;
285 }));
286 }
287
288
289 for (Future<?> future : futures) {
290 try {
291 future.get();
292 } catch (InterruptedException e) {
293 logger.error("Search interrupted", e);
294 currentThread().interrupt();
295 return;
296 }
297 }
298
299 showResult();
300 } catch (Exception e) {
301 logger.error("Search failed", e);
302 SwingUtilities.invokeLater(() -> progressBar.setString("Search failed"));
303 } finally {
304 executorService.shutdown();
305 SwingUtilities.invokeLater(() -> {
306 statButton.setEnabled(true);
307 cancelButton.setEnabled(false);
308 });
309 }
310 }
311
312 private void incrementProcessedFiles() {
313 processedFiles.incrementAndGet();
314 updateProgress();
315 }
316
317 private void updateProgress() {
318 if (totalFiles > 0) {
319 SwingUtilities.invokeLater(() -> {
320 int processed = processedFiles.get();
321 int percentage = (int) (processed * 100.0 / totalFiles);
322 progressBar.setValue(percentage);
323 progressBar.setString(String.format("Processing: %d/%d files (%d%%)", processed, totalFiles, percentage));
324 });
325 }
326 }
327 }
328 }