GameSentenceMiner 2.15.10__py3-none-any.whl → 2.15.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/anki.py +31 -0
- GameSentenceMiner/ocr/owocr_helper.py +5 -5
- GameSentenceMiner/web/templates/anki_stats.html +205 -0
- GameSentenceMiner/web/templates/stats.html +4 -0
- GameSentenceMiner/web/texthooking_page.py +50 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/RECORD +11 -11
- GameSentenceMiner/web/templates/text_replacements.html +0 -449
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -546,5 +546,36 @@ def start_monitoring_anki():
|
|
546
546
|
obs_thread.start()
|
547
547
|
|
548
548
|
|
549
|
+
# --- Anki Stats Kanji Extraction Utilities ---
|
550
|
+
|
551
|
+
def get_all_anki_first_field_kanji():
|
552
|
+
"""
|
553
|
+
Fetch all notes from Anki and extract unique kanji from the first field of each note.
|
554
|
+
Returns a set of kanji characters.
|
555
|
+
"""
|
556
|
+
from GameSentenceMiner.web.stats import is_kanji
|
557
|
+
try:
|
558
|
+
note_ids = invoke("findNotes", query="")
|
559
|
+
if not note_ids:
|
560
|
+
return set()
|
561
|
+
kanji_set = set()
|
562
|
+
batch_size = 1000
|
563
|
+
for i in range(0, len(note_ids), batch_size):
|
564
|
+
batch_ids = note_ids[i:i+batch_size]
|
565
|
+
notes_info = invoke("notesInfo", notes=batch_ids)
|
566
|
+
for note in notes_info:
|
567
|
+
fields = note.get("fields", {})
|
568
|
+
first_field = next(iter(fields.values()), None)
|
569
|
+
if first_field and "value" in first_field:
|
570
|
+
first_field_value = first_field["value"]
|
571
|
+
for char in first_field_value:
|
572
|
+
if is_kanji(char):
|
573
|
+
kanji_set.add(char)
|
574
|
+
return kanji_set
|
575
|
+
except Exception as e:
|
576
|
+
logger.error(f"Failed to fetch kanji from Anki: {e}")
|
577
|
+
return set()
|
578
|
+
|
579
|
+
|
549
580
|
if __name__ == "__main__":
|
550
581
|
print(invoke("getIntervals", cards=["1754694986036"]))
|
@@ -414,7 +414,7 @@ done = False
|
|
414
414
|
# Create a queue for tasks
|
415
415
|
second_ocr_queue = queue.Queue()
|
416
416
|
|
417
|
-
def get_ocr2_image(crop_coords, og_image, ocr2_engine=None):
|
417
|
+
def get_ocr2_image(crop_coords, og_image: Image.Image, ocr2_engine=None):
|
418
418
|
"""
|
419
419
|
Returns the image to use for the second OCR pass, cropping and scaling as needed.
|
420
420
|
Logic is unchanged, but code is refactored for clarity and maintainability.
|
@@ -424,10 +424,10 @@ def get_ocr2_image(crop_coords, og_image, ocr2_engine=None):
|
|
424
424
|
if not crop_coords or not get_ocr_optimize_second_scan():
|
425
425
|
return og_image
|
426
426
|
x1, y1, x2, y2 = crop_coords
|
427
|
-
x1 = min(max(0, x1),
|
428
|
-
y1 = min(max(0, y1),
|
429
|
-
x2 = min(max(0, x2),
|
430
|
-
y2 = min(max(0, y2),
|
427
|
+
x1 = min(max(0, x1), og_image.width)
|
428
|
+
y1 = min(max(0, y1), og_image.height)
|
429
|
+
x2 = min(max(0, x2), og_image.width)
|
430
|
+
y2 = min(max(0, y2), og_image.height)
|
431
431
|
og_image.save(os.path.join(get_temporary_directory(), "pre_oneocrcrop.png"))
|
432
432
|
return og_image.crop((x1, y1, x2, y2))
|
433
433
|
|
@@ -0,0 +1,205 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Anki vs GSM Kanji Stats</title>
|
7
|
+
<!-- Include Chart.js from a CDN -->
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
9
|
+
|
10
|
+
<!-- Include shared theme styles -->
|
11
|
+
{% include 'components/theme-styles.html' %}
|
12
|
+
|
13
|
+
<!-- Include shared CSS -->
|
14
|
+
<link rel="stylesheet" href="/static/css/shared.css">
|
15
|
+
|
16
|
+
<!-- Include stats-specific CSS -->
|
17
|
+
<link rel="stylesheet" href="/static/css/stats.css">
|
18
|
+
|
19
|
+
<!-- Include shared kanji grid CSS -->
|
20
|
+
<link rel="stylesheet" href="/static/css/kanji-grid.css">
|
21
|
+
</head>
|
22
|
+
<body>
|
23
|
+
|
24
|
+
<div class="container">
|
25
|
+
<h1>Anki & GSM Kanji Statistics</h1>
|
26
|
+
<div style="text-align: center;">
|
27
|
+
<p>Must have <a href="https://ankiweb.net/shared/info/2055492159">AnkiConnect</a></p>
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<!-- Include shared navigation -->
|
31
|
+
{% include 'components/navigation.html' %}
|
32
|
+
|
33
|
+
<!-- Dashboard Statistics Sections -->
|
34
|
+
<div class="dashboard-container">
|
35
|
+
<!-- Missing High-Frequency Kanji Card -->
|
36
|
+
<div class="dashboard-card current-game" id="missingKanjiCard">
|
37
|
+
<div class="dashboard-card-header">
|
38
|
+
<div>
|
39
|
+
<h3 class="dashboard-card-title">
|
40
|
+
<span class="dashboard-card-icon">🈚</span>
|
41
|
+
Missing High-Frequency Kanji
|
42
|
+
</h3>
|
43
|
+
<p class="dashboard-card-subtitle">Kanji seen often in GSM but not present in your Anki collection</p>
|
44
|
+
</div>
|
45
|
+
<div class="dashboard-streak-indicator" id="missingKanjiStreak" style="display: none;">
|
46
|
+
<span id="missingStreakValue">0</span> to learn
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<div class="dashboard-stats-grid" id="missingKanjiStats">
|
51
|
+
<div class="dashboard-stat-item tooltip" data-tooltip="Number of high-frequency kanji missing from Anki">
|
52
|
+
<span class="dashboard-stat-value" id="missingKanjiCount">-</span>
|
53
|
+
<span class="dashboard-stat-label">Missing Kanji</span>
|
54
|
+
</div>
|
55
|
+
<div class="dashboard-stat-item tooltip" data-tooltip="Total kanji in your Anki collection">
|
56
|
+
<span class="dashboard-stat-value" id="ankiTotalKanji">-</span>
|
57
|
+
<span class="dashboard-stat-label">Kanji in Anki</span>
|
58
|
+
</div>
|
59
|
+
<div class="dashboard-stat-item tooltip" data-tooltip="Total unique kanji seen in GSM">
|
60
|
+
<span class="dashboard-stat-value" id="gsmTotalKanji">-</span>
|
61
|
+
<span class="dashboard-stat-label">Kanji in GSM</span>
|
62
|
+
</div>
|
63
|
+
<div class="dashboard-stat-item tooltip" data-tooltip="Percentage of GSM kanji covered by Anki">
|
64
|
+
<span class="dashboard-stat-value" id="ankiCoverage">-</span>
|
65
|
+
<span class="dashboard-stat-label">Coverage %</span>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
|
69
|
+
<div class="dashboard-progress-section">
|
70
|
+
<div class="dashboard-progress-title">Missing Kanji Grid</div>
|
71
|
+
<div id="missingKanjiGridContainer">
|
72
|
+
<div id="missingKanjiGrid" class="kanji-grid"></div>
|
73
|
+
<div class="kanji-legend">
|
74
|
+
<span>Rarely Seen</span>
|
75
|
+
<div class="kanji-legend-item" style="background-color: #ebedf0;" title="No encounters"></div>
|
76
|
+
<div class="kanji-legend-item" style="background-color: #e6342e;" title="Seen once"></div>
|
77
|
+
<div class="kanji-legend-item" style="background-color: #e6dc2e;" title="Occasionally seen"></div>
|
78
|
+
<div class="kanji-legend-item" style="background-color: #3be62f;" title="Frequently seen"></div>
|
79
|
+
<div class="kanji-legend-item" style="background-color: #2ee6e0;" title="Most frequently seen"></div>
|
80
|
+
<span>Frequently Seen</span>
|
81
|
+
</div>
|
82
|
+
</div>
|
83
|
+
</div>
|
84
|
+
</div>
|
85
|
+
|
86
|
+
</div>
|
87
|
+
|
88
|
+
<!-- Loading/Error states for dashboard -->
|
89
|
+
<div class="dashboard-loading" id="ankiStatsLoading" style="display: none;">
|
90
|
+
<div class="spinner"></div>
|
91
|
+
<span>Loading Anki statistics...</span>
|
92
|
+
</div>
|
93
|
+
|
94
|
+
<div class="dashboard-error" id="ankiStatsError" style="display: none;">
|
95
|
+
<div class="dashboard-error-icon">⚠️</div>
|
96
|
+
<div class="dashboard-error-message">Failed to load Anki statistics</div>
|
97
|
+
<button class="dashboard-retry-btn" data-action="loadAnkiStats">Retry</button>
|
98
|
+
</div>
|
99
|
+
|
100
|
+
|
101
|
+
<!-- Settings Modal -->
|
102
|
+
<div id="ankiSettingsModal" class="modal">
|
103
|
+
<div class="modal-content">
|
104
|
+
<div class="modal-header">
|
105
|
+
<h3>Anki Statistics Settings</h3>
|
106
|
+
<span class="close-btn" id="closeAnkiSettingsModal">×</span>
|
107
|
+
</div>
|
108
|
+
<div class="modal-body">
|
109
|
+
<p style="color: var(--text-secondary); margin-bottom: 20px;">
|
110
|
+
Configure how Anki statistics and kanji analysis are calculated.
|
111
|
+
</p>
|
112
|
+
|
113
|
+
<form id="ankiSettingsForm">
|
114
|
+
<div style="margin-bottom: 20px;">
|
115
|
+
<label for="frequencyThreshold" style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
|
116
|
+
Minimum Frequency Threshold
|
117
|
+
</label>
|
118
|
+
<input
|
119
|
+
type="number"
|
120
|
+
id="frequencyThreshold"
|
121
|
+
name="frequency_threshold"
|
122
|
+
min="1"
|
123
|
+
max="1000"
|
124
|
+
style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
|
125
|
+
placeholder="10"
|
126
|
+
>
|
127
|
+
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
|
128
|
+
Minimum times a kanji must appear in GSM to be considered for analysis (1-1000)
|
129
|
+
</small>
|
130
|
+
</div>
|
131
|
+
|
132
|
+
<div style="margin-bottom: 20px;">
|
133
|
+
<label for="coverageTarget" style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
|
134
|
+
Coverage Target (%)
|
135
|
+
</label>
|
136
|
+
<input
|
137
|
+
type="number"
|
138
|
+
id="coverageTarget"
|
139
|
+
name="coverage_target"
|
140
|
+
min="50"
|
141
|
+
max="100"
|
142
|
+
style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
|
143
|
+
placeholder="95"
|
144
|
+
>
|
145
|
+
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
|
146
|
+
Target percentage coverage for Anki collection (50-100%)
|
147
|
+
</small>
|
148
|
+
</div>
|
149
|
+
|
150
|
+
<div style="margin-bottom: 20px;">
|
151
|
+
<label for="learningGoal" style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
|
152
|
+
Daily Learning Goal
|
153
|
+
</label>
|
154
|
+
<input
|
155
|
+
type="number"
|
156
|
+
id="learningGoal"
|
157
|
+
name="learning_goal"
|
158
|
+
min="1"
|
159
|
+
max="50"
|
160
|
+
style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
|
161
|
+
placeholder="5"
|
162
|
+
>
|
163
|
+
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
|
164
|
+
Target number of new kanji to learn per day (1-50)
|
165
|
+
</small>
|
166
|
+
</div>
|
167
|
+
|
168
|
+
<div style="margin-bottom: 20px;">
|
169
|
+
<label for="priorityMode" style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
|
170
|
+
Priority Mode
|
171
|
+
</label>
|
172
|
+
<select
|
173
|
+
id="priorityMode"
|
174
|
+
name="priority_mode"
|
175
|
+
style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
|
176
|
+
>
|
177
|
+
<option value="frequency">By Frequency</option>
|
178
|
+
<option value="jlpt">By JLPT Level</option>
|
179
|
+
<option value="grade">By School Grade</option>
|
180
|
+
<option value="mixed">Mixed Approach</option>
|
181
|
+
</select>
|
182
|
+
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
|
183
|
+
How to prioritize kanji for learning recommendations
|
184
|
+
</small>
|
185
|
+
</div>
|
186
|
+
</form>
|
187
|
+
|
188
|
+
<div id="ankiSettingsError" style="display: none; background: var(--danger-color); color: white; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 14px;"></div>
|
189
|
+
<div id="ankiSettingsSuccess" style="display: none; background: var(--success-color); color: white; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 14px;"></div>
|
190
|
+
</div>
|
191
|
+
<div class="modal-footer">
|
192
|
+
<button id="cancelAnkiSettingsBtn" class="cancel-btn">Cancel</button>
|
193
|
+
<button id="saveAnkiSettingsBtn" class="confirm-delete-btn">Save Settings</button>
|
194
|
+
</div>
|
195
|
+
</div>
|
196
|
+
</div>
|
197
|
+
</div>
|
198
|
+
|
199
|
+
<!-- Include shared JavaScript first (required dependency for anki_stats.js) -->
|
200
|
+
<script src="/static/js/shared.js"></script>
|
201
|
+
<script src="/static/js/kanji-grid.js"></script>
|
202
|
+
<script src="/static/js/anki_stats.js"></script>
|
203
|
+
|
204
|
+
</body>
|
205
|
+
</html>
|
@@ -15,6 +15,9 @@
|
|
15
15
|
|
16
16
|
<!-- Include stats-specific CSS -->
|
17
17
|
<link rel="stylesheet" href="/static/css/stats.css">
|
18
|
+
|
19
|
+
<!-- Include shared kanji grid CSS -->
|
20
|
+
<link rel="stylesheet" href="/static/css/kanji-grid.css">
|
18
21
|
</head>
|
19
22
|
<body>
|
20
23
|
|
@@ -324,6 +327,7 @@
|
|
324
327
|
|
325
328
|
<!-- Include shared JavaScript first (required dependency for stats.js) -->
|
326
329
|
<script src="/static/js/shared.js"></script>
|
330
|
+
<script src="/static/js/kanji-grid.js"></script>
|
327
331
|
<script src="/static/js/stats.js"></script>
|
328
332
|
|
329
333
|
</body>
|
@@ -263,12 +263,62 @@ def stats():
|
|
263
263
|
"""Renders the stats page."""
|
264
264
|
return render_template('stats.html')
|
265
265
|
|
266
|
+
@app.route('/api/anki_stats')
|
267
|
+
def api_anki_stats():
|
268
|
+
"""
|
269
|
+
API endpoint to provide Anki vs GSM kanji stats for the frontend.
|
270
|
+
Returns:
|
271
|
+
{
|
272
|
+
"missing_kanji": [ { "kanji": "漢", "frequency": 42 }, ... ],
|
273
|
+
"anki_kanji_count": 123,
|
274
|
+
"gsm_kanji_count": 456,
|
275
|
+
"coverage_percent": 27.0
|
276
|
+
}
|
277
|
+
"""
|
278
|
+
from GameSentenceMiner.anki import get_all_anki_first_field_kanji
|
279
|
+
from GameSentenceMiner.web.stats import calculate_kanji_frequency, is_kanji
|
280
|
+
from GameSentenceMiner.util.db import GameLinesTable
|
281
|
+
|
282
|
+
# Get all GSM lines and calculate kanji frequency
|
283
|
+
all_lines = GameLinesTable.all()
|
284
|
+
gsm_kanji_stats = calculate_kanji_frequency(all_lines)
|
285
|
+
gsm_kanji_list = gsm_kanji_stats.get("kanji_data", [])
|
286
|
+
gsm_kanji_set = set([k["kanji"] for k in gsm_kanji_list])
|
287
|
+
|
288
|
+
# Get all kanji in Anki (first field only)
|
289
|
+
anki_kanji_set = get_all_anki_first_field_kanji()
|
290
|
+
|
291
|
+
# Find missing kanji (in GSM but not in Anki)
|
292
|
+
missing_kanji = [
|
293
|
+
{"kanji": k["kanji"], "frequency": k["frequency"]}
|
294
|
+
for k in gsm_kanji_list if k["kanji"] not in anki_kanji_set
|
295
|
+
]
|
296
|
+
|
297
|
+
# Sort missing kanji by frequency descending
|
298
|
+
missing_kanji.sort(key=lambda x: x["frequency"], reverse=True)
|
299
|
+
|
300
|
+
# Coverage stats
|
301
|
+
anki_kanji_count = len(anki_kanji_set)
|
302
|
+
gsm_kanji_count = len(gsm_kanji_set)
|
303
|
+
coverage_percent = (anki_kanji_count / gsm_kanji_count * 100) if gsm_kanji_count else 0.0
|
304
|
+
|
305
|
+
return jsonify({
|
306
|
+
"missing_kanji": missing_kanji,
|
307
|
+
"anki_kanji_count": anki_kanji_count,
|
308
|
+
"gsm_kanji_count": gsm_kanji_count,
|
309
|
+
"coverage_percent": round(coverage_percent, 1)
|
310
|
+
})
|
266
311
|
|
267
312
|
@app.route('/search')
|
268
313
|
def search():
|
269
314
|
"""Renders the search page."""
|
270
315
|
return render_template('search.html')
|
271
316
|
|
317
|
+
@app.route('/anki_stats')
|
318
|
+
def anki_stats():
|
319
|
+
"""Renders the Anki statistics page."""
|
320
|
+
return render_template('anki_stats.html')
|
321
|
+
|
272
322
|
|
273
323
|
def get_selected_lines():
|
274
324
|
return [item.line for item in event_manager if item.checked]
|
@@ -1,5 +1,5 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=
|
2
|
+
GameSentenceMiner/anki.py,sha256=rm9JuRP-1Eba2wcVQ2PZUMB5P9UMEZ99Fh371K0Qfhk,26319
|
3
3
|
GameSentenceMiner/config_gui.py,sha256=i79PrY2pP8_VKvIL7uoDv5cgHvCCQBIe0mS_YnX2AVg,140792
|
4
4
|
GameSentenceMiner/gametext.py,sha256=fgBgLchezpauWELE9Y5G3kVCLfAneD0X4lJFoI3FYbs,10351
|
5
5
|
GameSentenceMiner/gsm.py,sha256=t2GAhMwVEHUzCdqM4tIgAzBUvNmt_Gec515iePacD6k,31945
|
@@ -22,7 +22,7 @@ GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
22
22
|
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=Ov04c-nKzh3sADxO-5JyZWVe4DlrHM9edM9tc7-97Jo,5970
|
23
23
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
24
24
|
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Rm1_nuZotJhfOfoJ_3mesh9udtOBjYqKhnAvSief6fo,29181
|
25
|
-
GameSentenceMiner/ocr/owocr_helper.py,sha256=
|
25
|
+
GameSentenceMiner/ocr/owocr_helper.py,sha256=wsI9HaDFcP9is71wf0YoLjf-FjgQ2Dps-e1e-l-HDl0,31722
|
26
26
|
GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
|
27
27
|
GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
|
28
28
|
GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
|
@@ -60,7 +60,7 @@ GameSentenceMiner/web/database_api.py,sha256=kcyTWPuw_qtrK5qBzCFTIP0tqIvPmL-NEti
|
|
60
60
|
GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZsA,5119
|
61
61
|
GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
|
62
62
|
GameSentenceMiner/web/stats.py,sha256=daSSxWlumAyqVVtX10qHESF-tZYwCcFMp8qZA5AE0nI,22066
|
63
|
-
GameSentenceMiner/web/texthooking_page.py,sha256=
|
63
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=gKCajF_SrIXdiLOTMxtSziXuQ7T4I7B0iBKqIhcN_Og,12943
|
64
64
|
GameSentenceMiner/web/websockets.py,sha256=IwwQo6VtgPqeOuc-datgfJyLpX3LwB2MISDqA6EkiSA,4131
|
65
65
|
GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
66
66
|
GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
|
@@ -71,16 +71,16 @@ GameSentenceMiner/web/static/site.webmanifest,sha256=kaeNT-FjFt-T7JGzOhXH7YSqsrD
|
|
71
71
|
GameSentenceMiner/web/static/style.css,sha256=bPZK0NVMuyRl5NNDuT7ZTzVLKlvSsdmeVHmAW4y5FM0,7001
|
72
72
|
GameSentenceMiner/web/static/web-app-manifest-192x192.png,sha256=EfSNnBmsSaLfESbkGfYwbKzcjKOdzuWo18ABADfN974,51117
|
73
73
|
GameSentenceMiner/web/static/web-app-manifest-512x512.png,sha256=wyqgCWCrLEUxSRXmaA3iJEESd-vM-ZmlTtZFBY4V8Pk,230819
|
74
|
+
GameSentenceMiner/web/templates/anki_stats.html,sha256=XXONeWFhMA6KZQx0gBJyxDKxLnX1zYTGoGhZlC7kbpA,10784
|
74
75
|
GameSentenceMiner/web/templates/database.html,sha256=iEJWQvvH_RGWmHuFx0iwNeamBV5FoVxZgFKgfm-4zc4,13582
|
75
76
|
GameSentenceMiner/web/templates/index.html,sha256=LqXZx7-NE42pXSpHNZ3To680rD-vt9wEJoFYBlgp1qU,216923
|
76
77
|
GameSentenceMiner/web/templates/search.html,sha256=Fat3hOjQwkYBbdFhgWzRzZ5iEB78-2_0LpT7uK2aURE,3701
|
77
|
-
GameSentenceMiner/web/templates/stats.html,sha256=
|
78
|
-
GameSentenceMiner/web/templates/text_replacements.html,sha256=rB6mUvzzdbAlNV0dEukZlec0sXgRarBZw8Qh_eWRErE,16694
|
78
|
+
GameSentenceMiner/web/templates/stats.html,sha256=jXHhp4fcSHgZ5kNGaFX5ArCf-4WCsktrw3uvV7lctPI,16827
|
79
79
|
GameSentenceMiner/web/templates/utility.html,sha256=KtqnZUMAYs5XsEdC9Tlsd40NKAVic0mu6sh-ReMDJpU,16940
|
80
80
|
GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
81
|
-
gamesentenceminer-2.15.
|
82
|
-
gamesentenceminer-2.15.
|
83
|
-
gamesentenceminer-2.15.
|
84
|
-
gamesentenceminer-2.15.
|
85
|
-
gamesentenceminer-2.15.
|
86
|
-
gamesentenceminer-2.15.
|
81
|
+
gamesentenceminer-2.15.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
82
|
+
gamesentenceminer-2.15.11.dist-info/METADATA,sha256=ZDnbGQL2dL520Tzv5Cb_EpfLbOYzxLDz8Iqb9YwuzbI,7349
|
83
|
+
gamesentenceminer-2.15.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
84
|
+
gamesentenceminer-2.15.11.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
85
|
+
gamesentenceminer-2.15.11.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
86
|
+
gamesentenceminer-2.15.11.dist-info/RECORD,,
|
@@ -1,449 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html lang="en">
|
3
|
-
<head>
|
4
|
-
<meta charset="UTF-8">
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
-
<title>Text Error Fixes (Electron)</title>
|
7
|
-
<link rel="stylesheet" href="/static/style.css">
|
8
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
9
|
-
<style>
|
10
|
-
:root {
|
11
|
-
/* Light theme colors */
|
12
|
-
--bg-primary: #f8f9fa;
|
13
|
-
--bg-secondary: #ffffff;
|
14
|
-
--bg-tertiary: #e9ecef;
|
15
|
-
--text-primary: #212529;
|
16
|
-
--text-secondary: #495057;
|
17
|
-
--text-tertiary: #6c757d;
|
18
|
-
--border-color: #dee2e6;
|
19
|
-
--shadow-color: rgba(0, 0, 0, 0.08);
|
20
|
-
--accent-color: #007bff;
|
21
|
-
--success-color: #28a745;
|
22
|
-
--warning-color: #ffc107;
|
23
|
-
--danger-color: #dc3545;
|
24
|
-
--info-color: #17a2b8;
|
25
|
-
}
|
26
|
-
|
27
|
-
[data-theme="dark"] {
|
28
|
-
/* Dark theme colors */
|
29
|
-
--bg-primary: #1a1a1a;
|
30
|
-
--bg-secondary: #2d2d2d;
|
31
|
-
--bg-tertiary: #3a3a3a;
|
32
|
-
--text-primary: #e1e1e1;
|
33
|
-
--text-secondary: #b8b8b8;
|
34
|
-
--text-tertiary: #8a8a8a;
|
35
|
-
--border-color: #404040;
|
36
|
-
--shadow-color: rgba(0, 0, 0, 0.3);
|
37
|
-
--accent-color: #4dabf7;
|
38
|
-
--success-color: #51cf66;
|
39
|
-
--warning-color: #ffd43b;
|
40
|
-
--danger-color: #ff6b6b;
|
41
|
-
--info-color: #22b8cf;
|
42
|
-
}
|
43
|
-
|
44
|
-
@media (prefers-color-scheme: dark) {
|
45
|
-
:root:not([data-theme="light"]) {
|
46
|
-
/* Auto dark mode colors */
|
47
|
-
--bg-primary: #1a1a1a;
|
48
|
-
--bg-secondary: #2d2d2d;
|
49
|
-
--bg-tertiary: #3a3a3a;
|
50
|
-
--text-primary: #e1e1e1;
|
51
|
-
--text-secondary: #b8b8b8;
|
52
|
-
--text-tertiary: #8a8a8a;
|
53
|
-
--border-color: #404040;
|
54
|
-
--shadow-color: rgba(0, 0, 0, 0.3);
|
55
|
-
--accent-color: #4dabf7;
|
56
|
-
--success-color: #51cf66;
|
57
|
-
--warning-color: #ffd43b;
|
58
|
-
--danger-color: #ff6b6b;
|
59
|
-
--info-color: #22b8cf;
|
60
|
-
}
|
61
|
-
}
|
62
|
-
|
63
|
-
.nav-link {
|
64
|
-
display: inline-block;
|
65
|
-
padding: 8px 16px;
|
66
|
-
background-color: var(--bg-tertiary);
|
67
|
-
color: var(--text-primary);
|
68
|
-
text-decoration: none;
|
69
|
-
border-radius: 5px;
|
70
|
-
transition: all 0.3s ease;
|
71
|
-
border: 1px solid var(--border-color);
|
72
|
-
}
|
73
|
-
|
74
|
-
.nav-link:hover {
|
75
|
-
background-color: var(--accent-color);
|
76
|
-
color: var(--bg-secondary);
|
77
|
-
transform: translateY(-1px);
|
78
|
-
}
|
79
|
-
|
80
|
-
.theme-toggle {
|
81
|
-
background: var(--bg-tertiary);
|
82
|
-
border: 2px solid var(--border-color);
|
83
|
-
border-radius: 50%;
|
84
|
-
width: 40px;
|
85
|
-
height: 40px;
|
86
|
-
display: flex;
|
87
|
-
align-items: center;
|
88
|
-
justify-content: center;
|
89
|
-
cursor: pointer;
|
90
|
-
transition: all 0.3s ease;
|
91
|
-
color: var(--text-primary);
|
92
|
-
font-size: 18px;
|
93
|
-
margin-left: 15px;
|
94
|
-
}
|
95
|
-
|
96
|
-
.theme-toggle:hover {
|
97
|
-
background: var(--bg-primary);
|
98
|
-
transform: scale(1.1);
|
99
|
-
}
|
100
|
-
|
101
|
-
.theme-toggle:active {
|
102
|
-
transform: scale(0.95);
|
103
|
-
}
|
104
|
-
|
105
|
-
@media (max-width: 768px) {
|
106
|
-
.navigation {
|
107
|
-
padding: 10px;
|
108
|
-
flex-direction: column;
|
109
|
-
gap: 10px;
|
110
|
-
}
|
111
|
-
|
112
|
-
.navigation > div {
|
113
|
-
flex-direction: column;
|
114
|
-
gap: 10px;
|
115
|
-
}
|
116
|
-
|
117
|
-
.navigation .nav-link {
|
118
|
-
display: block !important;
|
119
|
-
text-align: center;
|
120
|
-
width: 100%;
|
121
|
-
}
|
122
|
-
|
123
|
-
.theme-toggle {
|
124
|
-
margin-left: 0;
|
125
|
-
align-self: center;
|
126
|
-
}
|
127
|
-
}
|
128
|
-
</style>
|
129
|
-
</head>
|
130
|
-
<body> <div class="container">
|
131
|
-
<h1>Text Error Fixes</h1>
|
132
|
-
|
133
|
-
<div class="navigation" style="display: flex; justify-content: center; align-items: center; margin-bottom: 30px; padding: 15px; background: var(--bg-secondary); border-radius: 8px; box-shadow: 0 2px 8px var(--shadow-color); border: 1px solid var(--border-color);">
|
134
|
-
<div style="display: flex; gap: 15px;">
|
135
|
-
<a href="/" class="nav-link">Home</a>
|
136
|
-
<a href="/stats" class="nav-link">Statistics</a>
|
137
|
-
<a href="/search" class="nav-link">Search</a>
|
138
|
-
<a href="/text_replacements_page" class="nav-link">Text Replacements</a>
|
139
|
-
</div>
|
140
|
-
<button class="theme-toggle" id="themeToggle" title="Toggle dark mode">
|
141
|
-
<span id="themeIcon">🌙</span>
|
142
|
-
</button>
|
143
|
-
</div>
|
144
|
-
|
145
|
-
<!-- Explanation Section -->
|
146
|
-
<div style="background: var(--bg-secondary); padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px var(--shadow-color); margin-bottom: 30px; border: 1px solid var(--border-color);">
|
147
|
-
<h2 style="color: var(--text-secondary); margin-bottom: 15px;">What are Text Replacements?</h2>
|
148
|
-
<p style="color: var(--text-primary); line-height: 1.6; margin-bottom: 10px;">
|
149
|
-
Text replacements help you automatically fix common errors and clean up text captured from games. This is especially useful for:
|
150
|
-
</p>
|
151
|
-
<ul style="color: var(--text-primary); line-height: 1.6; margin-left: 20px; margin-bottom: 15px;">
|
152
|
-
<li><strong>OCR Error Correction:</strong> Fix text recognition mistakes from screenshots</li>
|
153
|
-
<li><strong>Text Standardization:</strong> Replace inconsistent character names or terms</li>
|
154
|
-
<li><strong>Regex Pattern Support:</strong> Use "re:" prefix for advanced pattern matching (e.g., "re:.{3,}" for sequences)</li>
|
155
|
-
<li><strong>Game-Specific Fixes:</strong> Handle unique formatting issues from different games</li>
|
156
|
-
</ul>
|
157
|
-
<p style="color: var(--text-tertiary); font-size: 14px; margin: 0;">
|
158
|
-
<strong>Pro Tip:</strong> Replacements are applied in the order they appear. Use regex patterns for complex transformations.
|
159
|
-
</p>
|
160
|
-
</div>
|
161
|
-
|
162
|
-
<div class="control"> <button id="add-new-btn" class="button-blue">Add New Entry</button> <div class="search-container"> <input type="text" id="search-input" placeholder="Search key or value..." class="inputField"> <button id="search-button" class="button-green">Search</button> </div>
|
163
|
-
<button id="go-back-btn" class="button-gray">Go Back</button>
|
164
|
-
</div>
|
165
|
-
|
166
|
-
<div id="data-table-container" class="table-container"> <table id="data-table" class="data-table"> <thead>
|
167
|
-
<tr>
|
168
|
-
<th>
|
169
|
-
Text To Replace (Key)
|
170
|
-
</th>
|
171
|
-
<th>
|
172
|
-
Replacement Text (Value)
|
173
|
-
</th>
|
174
|
-
<th>
|
175
|
-
Actions
|
176
|
-
</th>
|
177
|
-
</tr>
|
178
|
-
</thead>
|
179
|
-
<tbody id="data-table-body">
|
180
|
-
</tbody>
|
181
|
-
</table>
|
182
|
-
<p id="no-entries-message" class="no-entries-message hidden">No entries found.</p> </div>
|
183
|
-
</div>
|
184
|
-
|
185
|
-
<div id="entry-modal" class="modal"> <div class="modal-content"> <span class="close-button">×</span> <h2 id="modal-title">Add New Entry</h2> <form id="entry-form">
|
186
|
-
<p>"re:" at the beginning = regex pattern (ex. re:.{3,}) </p>
|
187
|
-
<div class="form-group"> <label for="key-input" class="form-label">Text To Replace (Key):</label> <input type="text" id="key-input" name="key" required class="form-input"> <input type="hidden" id="original-key-input">
|
188
|
-
</div>
|
189
|
-
<div class="form-group"> <label for="value-input" class="form-label">Replacement Text (Value):</label> <input type="text" id="value-input" name="value" class="form-input">
|
190
|
-
</div>
|
191
|
-
<div class="flex-end"> <button type="submit" class="button-blue">Save Entry</button> </div>
|
192
|
-
</form>
|
193
|
-
</div>
|
194
|
-
</div>
|
195
|
-
|
196
|
-
<script>
|
197
|
-
|
198
|
-
let textData = {};
|
199
|
-
let data = {};
|
200
|
-
|
201
|
-
async function loadData() {
|
202
|
-
try {
|
203
|
-
const response = await fetch('/load-data');
|
204
|
-
if (response.ok) {
|
205
|
-
data = await response.json();
|
206
|
-
textData = data.args?.replacements || {};
|
207
|
-
} else {
|
208
|
-
console.error('Failed to load data from server');
|
209
|
-
}
|
210
|
-
} catch (error) {
|
211
|
-
console.error('Error loading data:', error);
|
212
|
-
}
|
213
|
-
renderTable();
|
214
|
-
}
|
215
|
-
|
216
|
-
async function saveData() {
|
217
|
-
try {
|
218
|
-
data.args.replacements = textData;
|
219
|
-
const response = await fetch('/save-data', {
|
220
|
-
method: 'POST',
|
221
|
-
headers: { 'Content-Type': 'application/json' },
|
222
|
-
body: JSON.stringify(data),
|
223
|
-
});
|
224
|
-
if (!response.ok) {
|
225
|
-
console.error('Failed to save data to server');
|
226
|
-
}
|
227
|
-
} catch (error) {
|
228
|
-
console.error('Error saving data:', error);
|
229
|
-
}
|
230
|
-
}
|
231
|
-
|
232
|
-
function renderTable(dataToRender = textData) {
|
233
|
-
const tableBody = document.getElementById('data-table-body');
|
234
|
-
let tableHtml = '';
|
235
|
-
const noEntriesMessage = document.getElementById('no-entries-message');
|
236
|
-
const dataTable = document.getElementById('data-table');
|
237
|
-
const keys = Object.keys(dataToRender);
|
238
|
-
|
239
|
-
if (keys.length === 0) {
|
240
|
-
noEntriesMessage.classList.remove('hidden');
|
241
|
-
dataTable.classList.add('hidden');
|
242
|
-
} else {
|
243
|
-
noEntriesMessage.classList.add('hidden');
|
244
|
-
dataTable.classList.remove('hidden');
|
245
|
-
keys.forEach(key => {
|
246
|
-
const value = dataToRender[key];
|
247
|
-
tableHtml += `
|
248
|
-
<tr>
|
249
|
-
<td>${escapeHTML(key)}</td> <td>${escapeHTML(value)}</td> <td>
|
250
|
-
<button class="action-button edit-btn" data-key="${escapeHTML(key)}">Edit</button>
|
251
|
-
<button class="action-button delete-button delete-btn" data-key="${escapeHTML(key)}">Delete</button>
|
252
|
-
</td>
|
253
|
-
</tr>
|
254
|
-
`;
|
255
|
-
});
|
256
|
-
tableBody.innerHTML = tableHtml;
|
257
|
-
document.querySelectorAll('.edit-btn').forEach(button => {
|
258
|
-
button.addEventListener('click', handleEditClick);
|
259
|
-
});
|
260
|
-
document.querySelectorAll('.delete-btn').forEach(button => {
|
261
|
-
button.addEventListener('click', handleDeleteClick);
|
262
|
-
});
|
263
|
-
}
|
264
|
-
}
|
265
|
-
|
266
|
-
function escapeHTML(str) {
|
267
|
-
const div = document.createElement('div');
|
268
|
-
div.appendChild(document.createTextNode(str));
|
269
|
-
return div.innerHTML;
|
270
|
-
}
|
271
|
-
|
272
|
-
const modal = document.getElementById('entry-modal');
|
273
|
-
const modalTitle = document.getElementById('modal-title');
|
274
|
-
const entryForm = document.getElementById('entry-form');
|
275
|
-
const keyInput = document.getElementById('key-input');
|
276
|
-
const valueInput = document.getElementById('value-input');
|
277
|
-
const originalKeyInput = document.getElementById('original-key-input');
|
278
|
-
const closeButton = document.querySelector('.close-button');
|
279
|
-
const addNewBtn = document.getElementById('add-new-btn');
|
280
|
-
|
281
|
-
addNewBtn.addEventListener('click', () => {
|
282
|
-
modalTitle.textContent = 'Add New Entry';
|
283
|
-
keyInput.value = '';
|
284
|
-
valueInput.value = '';
|
285
|
-
originalKeyInput.value = '';
|
286
|
-
keyInput.disabled = false;
|
287
|
-
modal.style.display = 'flex';
|
288
|
-
});
|
289
|
-
|
290
|
-
function handleEditClick(event) {
|
291
|
-
const keyToEdit = event.target.dataset.key;
|
292
|
-
const valueToEdit = textData[keyToEdit];
|
293
|
-
modalTitle.textContent = 'Edit Entry';
|
294
|
-
keyInput.value = keyToEdit;
|
295
|
-
valueInput.value = valueToEdit;
|
296
|
-
originalKeyInput.value = keyToEdit;
|
297
|
-
modal.style.display = 'flex';
|
298
|
-
}
|
299
|
-
|
300
|
-
closeButton.addEventListener('click', () => {
|
301
|
-
modal.style.display = 'none';
|
302
|
-
});
|
303
|
-
|
304
|
-
window.addEventListener('mousedown', (event) => {
|
305
|
-
if (event.target === modal) {
|
306
|
-
modal.style.display = 'none';
|
307
|
-
}
|
308
|
-
});
|
309
|
-
|
310
|
-
entryForm.addEventListener('submit', async (event) => {
|
311
|
-
event.preventDefault();
|
312
|
-
const key = keyInput.value.trim();
|
313
|
-
const value = valueInput.value.trim() || "";
|
314
|
-
const originalKey = originalKeyInput.value;
|
315
|
-
|
316
|
-
if (!key) {
|
317
|
-
// Basic validation
|
318
|
-
alert('Key and Value cannot be empty.');
|
319
|
-
return;
|
320
|
-
}
|
321
|
-
|
322
|
-
let keyEdited = false;
|
323
|
-
if (originalKey && originalKey !== key) {
|
324
|
-
delete textData[originalKey];
|
325
|
-
keyEdited = true;
|
326
|
-
}
|
327
|
-
|
328
|
-
if (originalKey) {
|
329
|
-
if (keyEdited) {
|
330
|
-
textData = { [key]: value, ...textData };
|
331
|
-
} else {
|
332
|
-
textData[key] = value;
|
333
|
-
}
|
334
|
-
} else {
|
335
|
-
if (textData.hasOwnProperty(key)) {
|
336
|
-
alert(`Key "${key}" already exists. Please use the Edit function to modify it.`);
|
337
|
-
return;
|
338
|
-
}
|
339
|
-
textData = { [key]: value, ...textData };
|
340
|
-
}
|
341
|
-
|
342
|
-
await saveData();
|
343
|
-
renderTable();
|
344
|
-
modal.style.display = 'none';
|
345
|
-
});
|
346
|
-
|
347
|
-
function handleDeleteClick(event) {
|
348
|
-
const keyToDelete = event.target.dataset.key;
|
349
|
-
if (confirm(`Are you sure you want to delete the entry with key "${keyToDelete}"?`)) {
|
350
|
-
if (textData.hasOwnProperty(keyToDelete)) {
|
351
|
-
delete textData[keyToDelete];
|
352
|
-
saveData();
|
353
|
-
renderTable();
|
354
|
-
}
|
355
|
-
}
|
356
|
-
}
|
357
|
-
|
358
|
-
const searchInput = document.getElementById('search-input');
|
359
|
-
const searchButton = document.getElementById('search-button');
|
360
|
-
|
361
|
-
function performSearch() {
|
362
|
-
const query = searchInput.value.toLowerCase();
|
363
|
-
const filteredData = {};
|
364
|
-
for (const key in textData) {
|
365
|
-
if (textData.hasOwnProperty(key)) {
|
366
|
-
const value = textData[key];
|
367
|
-
if (key.toLowerCase().includes(query) || value.toLowerCase().includes(query)) {
|
368
|
-
filteredData[key] = value;
|
369
|
-
}
|
370
|
-
}
|
371
|
-
}
|
372
|
-
renderTable(filteredData);
|
373
|
-
}
|
374
|
-
|
375
|
-
searchButton.addEventListener('click', performSearch);
|
376
|
-
searchInput.addEventListener('input', performSearch);
|
377
|
-
|
378
|
-
const goBackBtn = document.getElementById('go-back-btn');
|
379
|
-
goBackBtn.addEventListener('click', () => {
|
380
|
-
window.history.back();
|
381
|
-
});
|
382
|
-
|
383
|
-
loadData();
|
384
|
-
|
385
|
-
// Dark mode toggle functionality
|
386
|
-
function initializeThemeToggle() {
|
387
|
-
const themeToggle = document.getElementById('themeToggle');
|
388
|
-
const themeIcon = document.getElementById('themeIcon');
|
389
|
-
const documentElement = document.documentElement;
|
390
|
-
|
391
|
-
// Check for saved theme preference or default to browser preference
|
392
|
-
function getPreferredTheme() {
|
393
|
-
const savedTheme = localStorage.getItem('theme');
|
394
|
-
if (savedTheme) {
|
395
|
-
return savedTheme;
|
396
|
-
}
|
397
|
-
|
398
|
-
// Check browser preference
|
399
|
-
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
400
|
-
return 'dark';
|
401
|
-
}
|
402
|
-
|
403
|
-
return 'light';
|
404
|
-
}
|
405
|
-
|
406
|
-
// Apply theme
|
407
|
-
function applyTheme(theme) {
|
408
|
-
if (theme === 'dark') {
|
409
|
-
documentElement.setAttribute('data-theme', 'dark');
|
410
|
-
themeIcon.textContent = '☀️';
|
411
|
-
themeToggle.title = 'Switch to light mode';
|
412
|
-
} else {
|
413
|
-
documentElement.setAttribute('data-theme', 'light');
|
414
|
-
themeIcon.textContent = '🌙';
|
415
|
-
themeToggle.title = 'Switch to dark mode';
|
416
|
-
}
|
417
|
-
}
|
418
|
-
|
419
|
-
// Initialize theme
|
420
|
-
const currentTheme = getPreferredTheme();
|
421
|
-
applyTheme(currentTheme);
|
422
|
-
|
423
|
-
// Toggle theme on button click
|
424
|
-
themeToggle.addEventListener('click', () => {
|
425
|
-
const currentTheme = documentElement.getAttribute('data-theme');
|
426
|
-
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
427
|
-
|
428
|
-
applyTheme(newTheme);
|
429
|
-
localStorage.setItem('theme', newTheme);
|
430
|
-
});
|
431
|
-
|
432
|
-
// Listen for browser theme changes
|
433
|
-
if (window.matchMedia) {
|
434
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
435
|
-
mediaQuery.addEventListener('change', (e) => {
|
436
|
-
// Only auto-switch if user hasn't manually set a preference
|
437
|
-
if (!localStorage.getItem('theme')) {
|
438
|
-
applyTheme(e.matches ? 'dark' : 'light');
|
439
|
-
}
|
440
|
-
});
|
441
|
-
}
|
442
|
-
}
|
443
|
-
|
444
|
-
// Initialize theme toggle
|
445
|
-
initializeThemeToggle();
|
446
|
-
|
447
|
-
</script>
|
448
|
-
</body>
|
449
|
-
</html>
|
File without changes
|
{gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/entry_points.txt
RENAMED
File without changes
|
{gamesentenceminer-2.15.10.dist-info → gamesentenceminer-2.15.11.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|