Okoge-keys commited on
Commit
bd01ca9
·
verified ·
1 Parent(s): 6df9022

Upload 9 files

Browse files
20250315_SearchFiles/README.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ファイル検索アプリ
2
+
3
+ このアプリケーションは、指定したディレクトリ内のファイルからテキストを検索するためのWebアプリケーションです。あいまい検索に対応しており、サブディレクトリを含めた再帰的な検索も可能です。
4
+
5
+ ## 機能
6
+
7
+ - 指定したディレクトリ内のファイルからテキストを検索
8
+ - あいまい検索(入力した文字が順番に含まれていれば一致と判定)
9
+ - サブディレクトリを含めた再帰的な検索オプション
10
+ - 検索結果の一覧表示(ファイルパスと一致した行を表示)
11
+ - 検索結果から該当ファイルのディレクトリをエクスプローラーで開く機能
12
+ - フォルダ選択ダイアログによる検索ディレクトリの選択(ダイアログ表示の通知機能付き)
13
+ - 検索履歴の保存と再利用
14
+ - 前回の検索条件を自動的に復元
15
+ - 様々なファイル形式に対応(テキストファイル、PDF、Word、Excel、PowerPointなど)
16
+
17
+ ## インストール方法
18
+
19
+ 1. リポジトリをクローンまたはダウンロードします
20
+ 2. 必要なパッケージをインストールします
21
+
22
+ ```bash
23
+ pip install -r requirements.txt
24
+ ```
25
+
26
+ ## 必要なパッケージ
27
+
28
+ - Flask: Webアプリケーションフレームワーク
29
+ - tkinter: フォルダ選択ダイアログ用(Pythonに標準で含まれています)
30
+ - PyPDF2: PDFファイルからのテキスト抽出用
31
+ - python-docx: Wordファイルからのテキスト抽出用
32
+ - python-pptx: PowerPointファイルからのテキスト抽出用
33
+ - openpyxl: Excelファイルからのテキスト抽出用
34
+
35
+ ## 使用方法
36
+
37
+ 1. アプリケーションを起動します
38
+
39
+ ```bash
40
+ python app.py
41
+ ```
42
+
43
+ 2. ブラウザで `http://127.0.0.1:5000` にアクセスします
44
+ 3. 検索するディレクトリのパスを入力します
45
+ 4. 検索するテキストを入力します
46
+ 5. 必要に応じて「サブフォルダも検索する」オプションをチェックします
47
+ 6. 「検索」ボタンをクリックします
48
+
49
+ ## 注意事項
50
+
51
+ - 大量のファイルや大きなファイルを検索する場合、処理に時間がかかる場合があります
52
+ - バイナリファイルなど、テキストとして読み込めないファイルは検索対象外となります
53
+ - ファイルのエンコーディングによっては、正しく読み込めない場合があります
54
+ - フォルダ選択ダイアログが表示されない場合は、他のウィンドウの背後を確認してください
55
+
56
+ ## 技術スタック
57
+
58
+ - Flask: Webアプリケーションフレームワーク
59
+ - HTML/CSS/JavaScript: フロントエンド
60
+ - Python: バックエンド処理
20250315_SearchFiles/__pycache__/app.cpython-312.pyc ADDED
Binary file (4.56 kB). View file
 
20250315_SearchFiles/app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import subprocess
4
+ import sys
5
+ from flask import Flask, render_template, request, jsonify
6
+ import PyPDF2
7
+ import docx
8
+ from pptx import Presentation
9
+ from openpyxl import load_workbook
10
+
11
+ app = Flask(__name__)
12
+
13
+ def search_files(directory, search_text, recursive=False):
14
+ """
15
+ Search for files containing the specified text in the given directory.
16
+
17
+ Args:
18
+ directory (str): The directory path to search in
19
+ search_text (str): The text to search for
20
+ recursive (bool): Whether to search recursively in subdirectories
21
+
22
+ Returns:
23
+ list: A list of dictionaries containing file paths and matching lines
24
+ """
25
+ results = []
26
+
27
+ # Check if directory exists
28
+ if not os.path.isdir(directory):
29
+ return {"error": f"Directory '{directory}' does not exist"}
30
+
31
+ # Create a regex pattern for fuzzy search
32
+ # This will match text with some flexibility
33
+ pattern = ''.join(f'.*?{re.escape(c)}' for c in search_text)
34
+ regex = re.compile(pattern, re.IGNORECASE)
35
+
36
+ # Walk through the directory
37
+ if recursive:
38
+ walk_func = os.walk
39
+ else:
40
+ # For non-recursive search, create a generator that only yields the top directory
41
+ def walk_func(path):
42
+ yield (path, [d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))],
43
+ [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))])
44
+
45
+ for root, _, files in walk_func(directory):
46
+ for file in files:
47
+ file_path = os.path.join(root, file)
48
+ try:
49
+ content = ""
50
+ file_ext = os.path.splitext(file_path.lower())[1]
51
+
52
+ # PDFファイルの場合
53
+ if file_ext == '.pdf':
54
+ try:
55
+ with open(file_path, 'rb') as pdf_file:
56
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
57
+ for page_num in range(len(pdf_reader.pages)):
58
+ page = pdf_reader.pages[page_num]
59
+ content += page.extract_text() + "\n"
60
+ except Exception as e:
61
+ # PDFの読み込みに失敗した場合はスキップ
62
+ continue
63
+
64
+ # Wordファイルの場合
65
+ elif file_ext == '.docx':
66
+ try:
67
+ doc = docx.Document(file_path)
68
+ for para in doc.paragraphs:
69
+ content += para.text + "\n"
70
+ # テーブル内のテキストも抽出
71
+ for table in doc.tables:
72
+ for row in table.rows:
73
+ for cell in row.cells:
74
+ content += cell.text + "\n"
75
+ except Exception as e:
76
+ # Wordファイルの読み込みに失敗した場合はスキップ
77
+ continue
78
+
79
+ # Excelファイルの場合
80
+ elif file_ext == '.xlsx':
81
+ try:
82
+ wb = load_workbook(filename=file_path, read_only=True)
83
+ for sheet_name in wb.sheetnames:
84
+ sheet = wb[sheet_name]
85
+ content += f"Sheet: {sheet_name}\n"
86
+ for row in sheet.rows:
87
+ row_text = ""
88
+ for cell in row:
89
+ if cell.value is not None:
90
+ row_text += str(cell.value) + "\t"
91
+ content += row_text + "\n"
92
+ except Exception as e:
93
+ # Excelファイルの読み込みに失敗した場合はスキップ
94
+ continue
95
+
96
+ # PowerPointファイルの場合
97
+ elif file_ext == '.pptx':
98
+ try:
99
+ prs = Presentation(file_path)
100
+ for i, slide in enumerate(prs.slides):
101
+ content += f"Slide {i+1}:\n"
102
+ for shape in slide.shapes:
103
+ if hasattr(shape, "text"):
104
+ content += shape.text + "\n"
105
+ except Exception as e:
106
+ # PowerPointファイルの読み込みに失敗した場合はスキップ
107
+ continue
108
+
109
+ else:
110
+ # 通常のテキストファイル
111
+ try:
112
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
113
+ content = f.read()
114
+ except Exception as e:
115
+ # テキストファイルの読み込みに失敗した場合はスキップ
116
+ continue
117
+
118
+ # Check if the content matches the search pattern
119
+ if regex.search(content):
120
+ # Get the lines that match
121
+ lines = content.split('\n')
122
+ matching_lines = []
123
+
124
+ for i, line in enumerate(lines):
125
+ if regex.search(line):
126
+ # Add line number and content
127
+ matching_lines.append({
128
+ 'line_number': i + 1,
129
+ 'content': line.strip()
130
+ })
131
+
132
+ # Add to results if there are matching lines
133
+ if matching_lines:
134
+ results.append({
135
+ 'file_path': file_path,
136
+ 'relative_path': os.path.relpath(file_path, directory),
137
+ 'matching_lines': matching_lines[:5] # Limit to first 5 matches
138
+ })
139
+ except Exception as e:
140
+ # Skip files that can't be read as text
141
+ continue
142
+
143
+ return results
144
+
145
+ @app.route('/')
146
+ def index():
147
+ return render_template('index.html')
148
+
149
+ @app.route('/search', methods=['POST'])
150
+ def search():
151
+ data = request.json
152
+ directory = data.get('directory', '')
153
+ search_text = data.get('search_text', '')
154
+ recursive = data.get('recursive', False)
155
+
156
+ if not directory or not search_text:
157
+ return jsonify({"error": "Directory and search text are required"})
158
+
159
+ results = search_files(directory, search_text, recursive)
160
+ return jsonify(results)
161
+
162
+ @app.route('/open_directory', methods=['POST'])
163
+ def open_directory():
164
+ data = request.json
165
+ file_path = data.get('file_path', '')
166
+
167
+ if not file_path:
168
+ return jsonify({"error": "ファイルパスが指定されていません"})
169
+
170
+ try:
171
+ # ファイルパスからディレクトリパスを取得
172
+ directory_path = os.path.dirname(file_path)
173
+
174
+ # ディレクトリが存在するか確認
175
+ if not os.path.isdir(directory_path):
176
+ return jsonify({"error": f"ディレクトリ '{directory_path}' が存在しません"})
177
+
178
+ # Windowsの場合、explorerを使用してディレクトリを開く
179
+ # /select,"ファイルパス" を使用すると、ファイルを選択した状態でエクスプローラーが開きます
180
+ subprocess.Popen(f'explorer.exe /select,"{file_path}"')
181
+
182
+ return jsonify({"success": True, "message": f"ディレクトリを開きました: {directory_path}"})
183
+ except Exception as e:
184
+ return jsonify({"error": f"エラーが発生しました: {str(e)}"})
185
+
186
+ @app.route('/select_folder', methods=['POST'])
187
+ def select_folder():
188
+ data = request.json
189
+ initial_dir = data.get('initial_dir', '')
190
+
191
+ try:
192
+ # スクリプトの絶対パスを取得
193
+ script_dir = os.path.dirname(os.path.abspath(__file__))
194
+ script_path = os.path.join(script_dir, 'folder_dialog.py')
195
+
196
+ # 初期ディレクトリが指定されていて、存在する場合はそれを引数として渡す
197
+ cmd_args = [sys.executable, script_path]
198
+ if initial_dir and os.path.isdir(initial_dir):
199
+ cmd_args.append(initial_dir)
200
+
201
+ # サブプロセスとしてfolder_dialog.pyを実行(新しいウィンドウで実行)
202
+ startupinfo = None
203
+ if os.name == 'nt': # Windowsの場合
204
+ startupinfo = subprocess.STARTUPINFO()
205
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
206
+ startupinfo.wShowWindow = 1 # SW_SHOWNORMAL
207
+
208
+ process = subprocess.Popen(
209
+ cmd_args,
210
+ stdout=subprocess.PIPE,
211
+ stderr=subprocess.PIPE,
212
+ text=True,
213
+ startupinfo=startupinfo
214
+ )
215
+
216
+ # 標準出力と標準エラー出力を取得
217
+ stdout, stderr = process.communicate()
218
+
219
+ # エラーがある場合
220
+ if stderr:
221
+ return jsonify({"error": f"エラーが発生しました: {stderr}"})
222
+
223
+ # 標準出力から選択されたフォルダパスを取得
224
+ folder_path = stdout.strip()
225
+
226
+ # フォルダが選択されなかった場合
227
+ if not folder_path:
228
+ return jsonify({"message": "フォルダ選択がキャンセルされました"})
229
+
230
+ return jsonify({"folder_path": folder_path, "message": "フォルダが選択されました"})
231
+ except Exception as e:
232
+ return jsonify({"error": f"エラ��が発生しました: {str(e)}"})
233
+
234
+ if __name__ == '__main__':
235
+ app.run(debug=True)
20250315_SearchFiles/folder_dialog.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import tkinter as tk
5
+ from tkinter import filedialog
6
+ import sys
7
+ import os
8
+ import time
9
+
10
+ def select_folder(initial_dir=None):
11
+ """
12
+ フォルダ選択ダイアログを表示し、選択されたフォルダパスを標準出力に出力する
13
+
14
+ Args:
15
+ initial_dir (str, optional): 初期ディレクトリ
16
+ """
17
+ # tkinterのルートウィンドウを作成
18
+ root = tk.Tk()
19
+
20
+ # ウィンドウを最小化して、タスクバーに表示しない
21
+ root.iconify()
22
+
23
+ # ウィンドウを前面に表示
24
+ root.attributes('-topmost', True)
25
+
26
+ # 少し待機して、ウィンドウが確実に作成されるようにする
27
+ time.sleep(0.1)
28
+
29
+ # 初期ディレクトリが指定されていて、存在する場合はそこから開始
30
+ if initial_dir and os.path.isdir(initial_dir):
31
+ folder_path = filedialog.askdirectory(
32
+ parent=root,
33
+ initialdir=initial_dir,
34
+ title="フォルダを選択してください"
35
+ )
36
+ else:
37
+ # 指定がない場合はデフォルトの場所から開始
38
+ folder_path = filedialog.askdirectory(
39
+ parent=root,
40
+ title="フォルダを選択してください"
41
+ )
42
+
43
+ # 選択されたフォルダパスを標準出力に出力
44
+ if folder_path:
45
+ print(folder_path)
46
+
47
+ # tkinterのメインループを終了
48
+ root.destroy()
49
+
50
+ if __name__ == "__main__":
51
+ try:
52
+ # コマンドライン引数から初期ディレクトリを取得
53
+ initial_dir = sys.argv[1] if len(sys.argv) > 1 else None
54
+ select_folder(initial_dir)
55
+ except Exception as e:
56
+ # エラーが発生した場合は標準エラー出力に出力
57
+ sys.stderr.write(f"エラーが発生しました: {str(e)}")
58
+ sys.exit(1)
20250315_SearchFiles/launch.bat ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ start python app.py
4
+ start chrome --new-tab "http://127.0.0.1:5000"
5
+ pause
20250315_SearchFiles/launch_new.bat ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ if not "%minimized%"=="" goto :minimized
3
+ set minimized=true
4
+ start /min cmd /C "%~dpnx0"
5
+ exit
6
+
7
+ :minimized
8
+ cd your_folder_path
9
+ start python app.py
10
+ timeout /t 5
11
+ start chrome --new-tab "http://127.0.0.1:5000"
20250315_SearchFiles/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==2.3.3
2
+ werkzeug==2.3.7
3
+ PyPDF2==3.0.1
4
+ python-docx==0.8.11
5
+ python-pptx==0.6.21
6
+ openpyxl==3.1.2
20250315_SearchFiles/static/css/style.css ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Modern dark theme for programmers - inspired by Apple and Google design */
2
+ :root {
3
+ --primary-color: #4285f4;
4
+ --primary-light: #80b1ff;
5
+ --primary-dark: #0d5bdd;
6
+ --secondary-color: #34a853;
7
+ --secondary-light: #5cd889;
8
+ --secondary-dark: #1e8e3e;
9
+ --accent-color: #fbbc05;
10
+ --text-color: #e0e0e0;
11
+ --text-light: #9e9e9e;
12
+ --background-color: #1e1e1e;
13
+ --card-color: #2d2d2d;
14
+ --card-hover: #383838;
15
+ --input-bg: #3c3c3c;
16
+ --error-color: #ea4335;
17
+ --success-color: #34a853;
18
+ --border-radius: 8px;
19
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.1);
20
+ --transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
21
+ --font-mono: 'SF Mono', 'Consolas', 'Monaco', monospace;
22
+ }
23
+
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
26
+ line-height: 1.6;
27
+ margin: 0;
28
+ padding: 20px;
29
+ background-color: var(--background-color);
30
+ color: var(--text-color);
31
+ -webkit-font-smoothing: antialiased;
32
+ -moz-osx-font-smoothing: grayscale;
33
+ }
34
+
35
+ .container {
36
+ max-width: 1000px;
37
+ margin: 0 auto;
38
+ background-color: var(--card-color);
39
+ padding: 30px;
40
+ border-radius: var(--border-radius);
41
+ box-shadow: var(--box-shadow);
42
+ transition: var(--transition);
43
+ }
44
+
45
+ h1 {
46
+ color: var(--text-color);
47
+ text-align: center;
48
+ margin-bottom: 30px;
49
+ font-weight: 500;
50
+ letter-spacing: 0.5px;
51
+ }
52
+
53
+ .form-group {
54
+ margin-bottom: 24px;
55
+ }
56
+
57
+ label {
58
+ display: block;
59
+ margin-bottom: 10px;
60
+ font-weight: 500;
61
+ color: var(--text-color);
62
+ font-size: 15px;
63
+ }
64
+
65
+ input[type="text"] {
66
+ width: 100%;
67
+ padding: 12px 16px;
68
+ background-color: var(--input-bg);
69
+ border: 1px solid rgba(255, 255, 255, 0.1);
70
+ border-radius: var(--border-radius);
71
+ box-sizing: border-box;
72
+ transition: var(--transition);
73
+ color: var(--text-color);
74
+ font-size: 15px;
75
+ }
76
+
77
+ input[type="text"]:focus {
78
+ border-color: var(--primary-color);
79
+ outline: none;
80
+ box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
81
+ }
82
+
83
+ .checkbox-group {
84
+ margin: 24px 0;
85
+ display: flex;
86
+ align-items: center;
87
+ }
88
+
89
+ input[type="checkbox"] {
90
+ margin-right: 10px;
91
+ width: 20px;
92
+ height: 20px;
93
+ accent-color: var(--primary-color);
94
+ cursor: pointer;
95
+ }
96
+
97
+ button {
98
+ background-color: var(--primary-color);
99
+ color: white;
100
+ border: none;
101
+ padding: 12px 24px;
102
+ border-radius: var(--border-radius);
103
+ cursor: pointer;
104
+ font-size: 16px;
105
+ font-weight: 500;
106
+ transition: var(--transition);
107
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
108
+ letter-spacing: 0.3px;
109
+ }
110
+
111
+ button:hover {
112
+ background-color: var(--primary-dark);
113
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.25);
114
+ transform: translateY(-2px);
115
+ }
116
+
117
+ button:active {
118
+ transform: translateY(1px);
119
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
120
+ }
121
+
122
+ button:disabled {
123
+ background-color: #555555;
124
+ color: #888888;
125
+ cursor: not-allowed;
126
+ box-shadow: none;
127
+ transform: none;
128
+ }
129
+
130
+ #results {
131
+ margin-top: 40px;
132
+ }
133
+
134
+ .result-item {
135
+ margin-bottom: 20px;
136
+ padding: 20px;
137
+ border: 1px solid rgba(255, 255, 255, 0.1);
138
+ border-radius: var(--border-radius);
139
+ background-color: var(--card-color);
140
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
141
+ transition: var(--transition);
142
+ }
143
+
144
+ .result-item:hover {
145
+ background-color: var(--card-hover);
146
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
147
+ transform: translateY(-2px);
148
+ }
149
+
150
+ .file-path {
151
+ font-weight: 500;
152
+ margin-bottom: 16px;
153
+ word-break: break-all;
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: center;
157
+ color: var(--primary-light);
158
+ font-size: 15px;
159
+ }
160
+
161
+ .open-dir-button {
162
+ background-color: var(--secondary-color);
163
+ color: white;
164
+ border: none;
165
+ padding: 8px 16px;
166
+ border-radius: var(--border-radius);
167
+ cursor: pointer;
168
+ font-size: 14px;
169
+ margin-left: 12px;
170
+ transition: var(--transition);
171
+ }
172
+
173
+ .open-dir-button:hover {
174
+ background-color: var(--secondary-dark);
175
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
176
+ transform: translateY(-1px);
177
+ }
178
+
179
+ .folder-select-button {
180
+ background-color: var(--secondary-color);
181
+ color: white;
182
+ border: none;
183
+ padding: 12px 16px;
184
+ border-radius: var(--border-radius);
185
+ cursor: pointer;
186
+ font-size: 14px;
187
+ margin-left: 12px;
188
+ transition: var(--transition);
189
+ white-space: nowrap;
190
+ }
191
+
192
+ .folder-select-button:hover {
193
+ background-color: var(--secondary-dark);
194
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
195
+ transform: translateY(-1px);
196
+ }
197
+
198
+ .match-line {
199
+ font-family: var(--font-mono);
200
+ background-color: rgba(66, 133, 244, 0.1);
201
+ padding: 12px 16px;
202
+ border-left: 3px solid var(--primary-color);
203
+ margin: 8px 0;
204
+ border-radius: 0 4px 4px 0;
205
+ overflow-x: auto;
206
+ color: var(--text-color);
207
+ font-size: 14px;
208
+ }
209
+
210
+ .line-number {
211
+ color: var(--accent-color);
212
+ margin-right: 16px;
213
+ font-weight: 500;
214
+ user-select: none;
215
+ }
216
+
217
+ .loading {
218
+ text-align: center;
219
+ margin: 40px 0;
220
+ display: none;
221
+ color: var(--primary-light);
222
+ }
223
+
224
+ .loading p {
225
+ font-size: 16px;
226
+ font-weight: 500;
227
+ letter-spacing: 0.5px;
228
+ }
229
+
230
+ .error {
231
+ color: var(--error-color);
232
+ padding: 16px;
233
+ background-color: rgba(234, 67, 53, 0.15);
234
+ border-radius: var(--border-radius);
235
+ margin-bottom: 24px;
236
+ font-weight: 500;
237
+ border-left: 4px solid var(--error-color);
238
+ }
239
+
240
+ .search-history {
241
+ margin-top: 40px;
242
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
243
+ padding-top: 24px;
244
+ }
245
+
246
+ .search-history h3 {
247
+ color: var(--text-color);
248
+ margin-bottom: 16px;
249
+ font-weight: 500;
250
+ letter-spacing: 0.5px;
251
+ }
252
+
253
+ .history-item {
254
+ padding: 14px 16px;
255
+ margin-bottom: 8px;
256
+ background-color: var(--card-color);
257
+ border: 1px solid rgba(255, 255, 255, 0.1);
258
+ border-radius: var(--border-radius);
259
+ cursor: pointer;
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ transition: var(--transition);
264
+ }
265
+
266
+ .history-item:hover {
267
+ background-color: var(--card-hover);
268
+ transform: translateY(-1px);
269
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
270
+ }
271
+
272
+ .history-item strong {
273
+ color: var(--primary-light);
274
+ }
275
+
276
+ .history-item small {
277
+ color: var(--text-light);
278
+ margin-left: 10px;
279
+ font-size: 13px;
280
+ }
281
+
282
+ .history-delete {
283
+ color: var(--error-color);
284
+ cursor: pointer;
285
+ font-weight: bold;
286
+ margin-left: 12px;
287
+ padding: 4px 10px;
288
+ border-radius: 50%;
289
+ transition: var(--transition);
290
+ }
291
+
292
+ .history-delete:hover {
293
+ background-color: rgba(234, 67, 53, 0.2);
294
+ }
295
+
296
+ /* Custom scrollbar for webkit browsers */
297
+ ::-webkit-scrollbar {
298
+ width: 10px;
299
+ height: 10px;
300
+ }
301
+
302
+ ::-webkit-scrollbar-track {
303
+ background: var(--background-color);
304
+ border-radius: 4px;
305
+ }
306
+
307
+ ::-webkit-scrollbar-thumb {
308
+ background: #555;
309
+ border-radius: 4px;
310
+ }
311
+
312
+ ::-webkit-scrollbar-thumb:hover {
313
+ background: #777;
314
+ }
20250315_SearchFiles/templates/index.html ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ファイル検索アプリ</title>
7
+ <link rel="stylesheet" href="/static/css/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <h1>ファイル検索アプリ</h1>
12
+
13
+ <div id="error-message" class="error" style="display: none;"></div>
14
+
15
+ <div class="form-group">
16
+ <label for="directory">検索するフォルダのパス:</label>
17
+ <div style="display: flex; align-items: center;">
18
+ <input type="text" id="directory" placeholder="例: C:\Users\Documents" style="flex-grow: 1;">
19
+ <button id="folder-select-button" class="folder-select-button">フォルダを選択</button>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="form-group">
24
+ <label for="search-text">検索するテキスト:</label>
25
+ <input type="text" id="search-text" placeholder="検索したいテキストを入力">
26
+ </div>
27
+
28
+ <div class="checkbox-group">
29
+ <input type="checkbox" id="recursive">
30
+ <label for="recursive" style="display: inline;">サブフォルダも検索する</label>
31
+ </div>
32
+
33
+ <button id="search-button">検索</button>
34
+
35
+ <div class="loading" id="loading">
36
+ <p>検索中...</p>
37
+ </div>
38
+
39
+ <div id="results"></div>
40
+
41
+ <div id="search-history" class="search-history" style="display: none;">
42
+ <h3>検索履歴</h3>
43
+ <div id="history-list"></div>
44
+ </div>
45
+ </div>
46
+
47
+ <script>
48
+ document.addEventListener('DOMContentLoaded', function() {
49
+ const searchButton = document.getElementById('search-button');
50
+ const directoryInput = document.getElementById('directory');
51
+ const searchTextInput = document.getElementById('search-text');
52
+ const recursiveCheckbox = document.getElementById('recursive');
53
+ const resultsDiv = document.getElementById('results');
54
+ const loadingDiv = document.getElementById('loading');
55
+ const errorMessageDiv = document.getElementById('error-message');
56
+ const folderSelectButton = document.getElementById('folder-select-button');
57
+ const searchHistoryDiv = document.getElementById('search-history');
58
+ const historyListDiv = document.getElementById('history-list');
59
+
60
+ // 検索履歴の読み込み
61
+ let searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]');
62
+
63
+ // 前回の入力値を復元
64
+ const lastDirectory = localStorage.getItem('lastDirectory') || '';
65
+ const lastSearchText = localStorage.getItem('lastSearchText') || '';
66
+ const lastRecursive = localStorage.getItem('lastRecursive') === 'true';
67
+
68
+ if (lastDirectory) directoryInput.value = lastDirectory;
69
+ if (lastSearchText) searchTextInput.value = lastSearchText;
70
+ recursiveCheckbox.checked = lastRecursive;
71
+
72
+ // Helper function to escape HTML
73
+ function escapeHTML(str) {
74
+ return str
75
+ .replace(/&/g, '&amp;')
76
+ .replace(/</g, '&lt;')
77
+ .replace(/>/g, '&gt;')
78
+ .replace(/"/g, '&quot;')
79
+ .replace(/'/g, '&#039;');
80
+ }
81
+
82
+ // ディレクトリを開く関数
83
+ function openDirectory(filePath) {
84
+ fetch('/open_directory', {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ body: JSON.stringify({
90
+ file_path: filePath
91
+ })
92
+ })
93
+ .then(response => response.json())
94
+ .then(data => {
95
+ if (data.error) {
96
+ errorMessageDiv.textContent = data.error;
97
+ errorMessageDiv.style.display = 'block';
98
+ }
99
+ })
100
+ .catch(error => {
101
+ errorMessageDiv.textContent = 'エラーが発生しました: ' + error.message;
102
+ errorMessageDiv.style.display = 'block';
103
+ });
104
+ }
105
+
106
+ // 検索履歴に追加する関数
107
+ function addToSearchHistory(directory, searchText, recursive) {
108
+ // 同じ検索条件がある場合は削除
109
+ searchHistory = searchHistory.filter(item =>
110
+ !(item.directory === directory &&
111
+ item.searchText === searchText &&
112
+ item.recursive === recursive)
113
+ );
114
+
115
+ // 新しい検索を先頭に追加
116
+ searchHistory.unshift({
117
+ directory,
118
+ searchText,
119
+ recursive,
120
+ timestamp: new Date().toISOString()
121
+ });
122
+
123
+ // 履歴は最大20件まで
124
+ if (searchHistory.length > 20) {
125
+ searchHistory = searchHistory.slice(0, 20);
126
+ }
127
+
128
+ // ローカルストレージに保存
129
+ localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
130
+
131
+ // 表示を更新
132
+ updateSearchHistoryDisplay();
133
+ }
134
+
135
+ // 検索履歴の表示を更新する関数
136
+ function updateSearchHistoryDisplay() {
137
+ if (searchHistory.length === 0) {
138
+ searchHistoryDiv.style.display = 'none';
139
+ return;
140
+ }
141
+
142
+ searchHistoryDiv.style.display = 'block';
143
+
144
+ const historyHTML = searchHistory.map((item, index) => {
145
+ const date = new Date(item.timestamp);
146
+ const formattedDate = `${date.getFullYear()}/${(date.getMonth()+1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
147
+
148
+ return `<div class="history-item" data-index="${index}">
149
+ <div>
150
+ <strong>${item.searchText}</strong> in ${item.directory}
151
+ ${item.recursive ? '(サブフォルダ含む)' : ''}
152
+ <small>${formattedDate}</small>
153
+ </div>
154
+ <span class="history-delete" data-index="${index}">×</span>
155
+ </div>`;
156
+ }).join('');
157
+
158
+ historyListDiv.innerHTML = historyHTML;
159
+
160
+ // 履歴項目のクリックイベント
161
+ document.querySelectorAll('.history-item').forEach(item => {
162
+ item.addEventListener('click', function(e) {
163
+ if (e.target.classList.contains('history-delete')) return;
164
+
165
+ const index = parseInt(this.getAttribute('data-index'));
166
+ const historyItem = searchHistory[index];
167
+
168
+ directoryInput.value = historyItem.directory;
169
+ searchTextInput.value = historyItem.searchText;
170
+ recursiveCheckbox.checked = historyItem.recursive;
171
+
172
+ performSearch();
173
+ });
174
+ });
175
+
176
+ // 削除ボタンのクリックイベント
177
+ document.querySelectorAll('.history-delete').forEach(button => {
178
+ button.addEventListener('click', function(e) {
179
+ e.stopPropagation();
180
+ const index = parseInt(this.getAttribute('data-index'));
181
+
182
+ searchHistory.splice(index, 1);
183
+ localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
184
+
185
+ updateSearchHistoryDisplay();
186
+ });
187
+ });
188
+ }
189
+
190
+ // 検索を実行する関数
191
+ function performSearch() {
192
+ // Clear previous results and errors
193
+ resultsDiv.innerHTML = '';
194
+ errorMessageDiv.style.display = 'none';
195
+
196
+ const directory = directoryInput.value.trim();
197
+ const searchText = searchTextInput.value.trim();
198
+
199
+ if (!directory || !searchText) {
200
+ errorMessageDiv.textContent = 'フォルダのパスと検索テキストを入力してください。';
201
+ errorMessageDiv.style.display = 'block';
202
+ return;
203
+ }
204
+
205
+ // Show loading indicator
206
+ loadingDiv.style.display = 'block';
207
+
208
+ // 入力値を保存
209
+ localStorage.setItem('lastDirectory', directory);
210
+ localStorage.setItem('lastSearchText', searchText);
211
+ localStorage.setItem('lastRecursive', recursiveCheckbox.checked);
212
+
213
+ // 検索履歴に追加
214
+ addToSearchHistory(directory, searchText, recursiveCheckbox.checked);
215
+
216
+ // Send search request
217
+ fetch('/search', {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify({
223
+ directory: directory,
224
+ search_text: searchText,
225
+ recursive: recursiveCheckbox.checked
226
+ })
227
+ })
228
+ .then(response => response.json())
229
+ .then(data => {
230
+ // Hide loading indicator
231
+ loadingDiv.style.display = 'none';
232
+
233
+ if (data.error) {
234
+ errorMessageDiv.textContent = data.error;
235
+ errorMessageDiv.style.display = 'block';
236
+ return;
237
+ }
238
+
239
+ if (data.length === 0) {
240
+ resultsDiv.innerHTML = '<p>一致するファイルが見つかりませんでした。</p>';
241
+ return;
242
+ }
243
+
244
+ // Display results
245
+ const resultsHTML = data.map(result => {
246
+ const matchesHTML = result.matching_lines.map(line => {
247
+ return `<div class="match-line">
248
+ <span class="line-number">行 ${line.line_number}:</span>
249
+ ${escapeHTML(line.content)}
250
+ </div>`;
251
+ }).join('');
252
+
253
+ return `<div class="result-item">
254
+ <div class="file-path">
255
+ <span>${result.relative_path}</span>
256
+ <button class="open-dir-button" data-filepath="${result.file_path}">フォルダを開く</button>
257
+ </div>
258
+ ${matchesHTML}
259
+ </div>`;
260
+ }).join('');
261
+
262
+ resultsDiv.innerHTML = `
263
+ <h2>検索結果 (${data.length} ファイル)</h2>
264
+ ${resultsHTML}
265
+ `;
266
+
267
+ // フォルダを開くボタンにイベントリスナーを追加
268
+ document.querySelectorAll('.open-dir-button').forEach(button => {
269
+ button.addEventListener('click', function() {
270
+ const filePath = this.getAttribute('data-filepath');
271
+ openDirectory(filePath);
272
+ });
273
+ });
274
+ })
275
+ .catch(error => {
276
+ loadingDiv.style.display = 'none';
277
+ errorMessageDiv.textContent = 'エラーが発生しました: ' + error.message;
278
+ errorMessageDiv.style.display = 'block';
279
+ });
280
+ }
281
+
282
+ // フォルダ選択ボタンのイベントリスナー
283
+ folderSelectButton.addEventListener('click', function() {
284
+ // ボタンを無効化
285
+ folderSelectButton.disabled = true;
286
+ folderSelectButton.textContent = 'ダイアログを表示中...';
287
+
288
+ // 通知メッセージを表示
289
+ const notificationDiv = document.createElement('div');
290
+ notificationDiv.style.position = 'fixed';
291
+ notificationDiv.style.top = '50%';
292
+ notificationDiv.style.left = '50%';
293
+ notificationDiv.style.transform = 'translate(-50%, -50%)';
294
+ notificationDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
295
+ notificationDiv.style.color = 'white';
296
+ notificationDiv.style.padding = '20px';
297
+ notificationDiv.style.borderRadius = '5px';
298
+ notificationDiv.style.zIndex = '1000';
299
+ notificationDiv.style.textAlign = 'center';
300
+ notificationDiv.innerHTML = `
301
+ <p>フォルダ選択ダイアログが表示されます。</p>
302
+ <p>ダイアログが見つからない場合は、他のウィンドウの背後を確認してください。</p>
303
+ `;
304
+ document.body.appendChild(notificationDiv);
305
+
306
+ // 少し待ってから通知を消す
307
+ setTimeout(() => {
308
+ document.body.removeChild(notificationDiv);
309
+ }, 3000);
310
+
311
+ // フォルダ選択ダイアログを表示
312
+ fetch('/select_folder', {
313
+ method: 'POST',
314
+ headers: {
315
+ 'Content-Type': 'application/json',
316
+ },
317
+ body: JSON.stringify({
318
+ initial_dir: directoryInput.value.trim() || localStorage.getItem('lastDirectory') || ''
319
+ })
320
+ })
321
+ .then(response => response.json())
322
+ .then(data => {
323
+ // ボタンを元に戻す
324
+ folderSelectButton.disabled = false;
325
+ folderSelectButton.textContent = 'フォルダを選択';
326
+
327
+ if (data.error) {
328
+ errorMessageDiv.textContent = data.error;
329
+ errorMessageDiv.style.display = 'block';
330
+ return;
331
+ }
332
+
333
+ if (data.message) {
334
+ // 成功メッセージを表示
335
+ const successDiv = document.createElement('div');
336
+ successDiv.style.position = 'fixed';
337
+ successDiv.style.top = '50%';
338
+ successDiv.style.left = '50%';
339
+ successDiv.style.transform = 'translate(-50%, -50%)';
340
+ successDiv.style.backgroundColor = 'rgba(76, 175, 80, 0.9)';
341
+ successDiv.style.color = 'white';
342
+ successDiv.style.padding = '20px';
343
+ successDiv.style.borderRadius = '5px';
344
+ successDiv.style.zIndex = '1000';
345
+ successDiv.style.textAlign = 'center';
346
+ successDiv.textContent = data.message;
347
+ document.body.appendChild(successDiv);
348
+
349
+ // 2秒後に成功メッセージを消す
350
+ setTimeout(() => {
351
+ document.body.removeChild(successDiv);
352
+ }, 2000);
353
+ }
354
+
355
+ if (data.folder_path) {
356
+ directoryInput.value = data.folder_path;
357
+ localStorage.setItem('lastDirectory', data.folder_path);
358
+ }
359
+ })
360
+ .catch(error => {
361
+ // ボタンを元に戻す
362
+ folderSelectButton.disabled = false;
363
+ folderSelectButton.textContent = 'フォルダを選択';
364
+
365
+ errorMessageDiv.textContent = 'エラーが発生しました: ' + error.message;
366
+ errorMessageDiv.style.display = 'block';
367
+ });
368
+ });
369
+
370
+ // 検索ボタンのイベントリスナー
371
+ searchButton.addEventListener('click', function() {
372
+ performSearch();
373
+ });
374
+
375
+ // 初期表示時に検索履歴を表示
376
+ updateSearchHistoryDisplay();
377
+ });
378
+ </script>
379
+ </body>
380
+ </html>