GameSentenceMiner 2.21.25__tar.gz → 2.21.29__tar.gz

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.
Files changed (184) hide show
  1. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/ss_picker_qt.py +178 -36
  2. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/goals.js +21 -20
  3. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  4. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/PKG-INFO +1 -1
  5. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/pyproject.toml +1 -1
  6. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/__init__.py +0 -0
  7. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ai/__init__.py +0 -0
  8. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  9. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/anki.py +0 -0
  10. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/__init__.py +0 -0
  11. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/icon.png +0 -0
  12. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/icon128.png +0 -0
  13. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/icon256.png +0 -0
  14. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/icon32.png +0 -0
  15. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/icon512.png +0 -0
  16. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/icon64.png +0 -0
  17. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/assets/pickaxe.png +0 -0
  18. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/discord_rpc.py +0 -0
  19. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/gametext.py +0 -0
  20. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/gsm.py +0 -0
  21. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/live_stats.py +0 -0
  22. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/locales/en_us.json +0 -0
  23. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/locales/es_es.json +0 -0
  24. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/locales/ja_jp.json +0 -0
  25. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/locales/zh_cn.json +0 -0
  26. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/obs.py +0 -0
  27. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/__init__.py +0 -0
  28. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  29. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  30. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  31. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/owocr_area_selector_qt.py +0 -0
  32. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  33. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  34. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  35. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  36. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  37. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  38. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  39. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  40. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  41. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/tools/__init__.py +0 -0
  42. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  43. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/tools/furigana_filter_preview.py +0 -0
  44. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/tools/ss_selector.py +0 -0
  45. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/tools/window_transparency.py +0 -0
  46. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/__init__.py +0 -0
  47. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/anki_confirmation.py +0 -0
  48. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/anki_confirmation_qt.py +0 -0
  49. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/config_gui.py +0 -0
  50. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/config_gui_qt.py +0 -0
  51. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/furigana_filter_preview.py +0 -0
  52. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/furigana_filter_preview_qt.py +0 -0
  53. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/qt_main.py +0 -0
  54. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/screenshot_selector.py +0 -0
  55. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/ui/screenshot_selector_qt.py +0 -0
  56. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/__init__.py +0 -0
  57. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/audio_player.py +0 -0
  58. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/communication/__init__.py +0 -0
  59. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/communication/electron_ipc.py +0 -0
  60. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/communication/send.py +0 -0
  61. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/configuration.py +0 -0
  62. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron/__init__.py +0 -0
  63. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron/daily_rollup.py +0 -0
  64. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron/jiten_update.py +0 -0
  65. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron/populate_games.py +0 -0
  66. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron/run_crons.py +0 -0
  67. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron/setup_populate_games_cron.py +0 -0
  68. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/cron_table.py +0 -0
  69. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/db.py +0 -0
  70. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  71. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  72. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  73. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  74. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/electron_config.py +0 -0
  75. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/ffmpeg.py +0 -0
  76. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/games_table.py +0 -0
  77. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/get_overlay_coords.py +0 -0
  78. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/gsm_utils.py +0 -0
  79. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/hotkey.py +0 -0
  80. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/jiten_api_client.py +0 -0
  81. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/model.py +0 -0
  82. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/notification.py +0 -0
  83. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/stats_rollup_table.py +0 -0
  84. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/text_log.py +0 -0
  85. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
  86. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
  87. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/vad.py +0 -0
  88. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/__init__.py +0 -0
  89. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/anki_api_endpoints.py +0 -0
  90. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/database_api.py +0 -0
  91. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/events.py +0 -0
  92. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/goals_api.py +0 -0
  93. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/gsm_websocket.py +0 -0
  94. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/jiten_database_api.py +0 -0
  95. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/rollup_stats.py +0 -0
  96. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/service.py +0 -0
  97. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/__init__.py +0 -0
  98. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  99. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/dashboard-shared.css +0 -0
  100. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/goals.css +0 -0
  101. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/kanji-grid.css +0 -0
  102. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/loading-skeleton.css +0 -0
  103. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/overview.css +0 -0
  104. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/popups-shared.css +0 -0
  105. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/search.css +0 -0
  106. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/shared.css +0 -0
  107. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/css/stats.css +0 -0
  108. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  109. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/favicon.ico +0 -0
  110. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/favicon.svg +0 -0
  111. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/anki_stats.js +0 -0
  112. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/components/bar-chart.js +0 -0
  113. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-bulk-operations.js +0 -0
  114. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-game-data.js +0 -0
  115. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-game-operations.js +0 -0
  116. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-helpers.js +0 -0
  117. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-jiten-integration.js +0 -0
  118. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-popups.js +0 -0
  119. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-tabs.js +0 -0
  120. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database-text-management.js +0 -0
  121. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/database.js +0 -0
  122. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/heatmap.js +0 -0
  123. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/kanji-grid.js +0 -0
  124. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/overview.js +0 -0
  125. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/regex-patterns.js +0 -0
  126. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/search.js +0 -0
  127. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/shared.js +0 -0
  128. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/js/stats.js +0 -0
  129. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  130. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/style.css +0 -0
  131. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  132. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  133. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/stats.py +0 -0
  134. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/stats_api.py +0 -0
  135. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/anki_stats.html +0 -0
  136. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/date-range.html +0 -0
  137. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/html-head.html +0 -0
  138. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/js-config.html +0 -0
  139. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +0 -0
  140. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +0 -0
  141. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/grade.json +0 -0
  142. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +0 -0
  143. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +0 -0
  144. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +0 -0
  145. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +0 -0
  146. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +0 -0
  147. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +0 -0
  148. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +0 -0
  149. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +0 -0
  150. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +0 -0
  151. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +0 -0
  152. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +0 -0
  153. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +0 -0
  154. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +0 -0
  155. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +0 -0
  156. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +0 -0
  157. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +0 -0
  158. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +0 -0
  159. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +0 -0
  160. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +0 -0
  161. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +0 -0
  162. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +0 -0
  163. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/navigation.html +0 -0
  164. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/popups.html +0 -0
  165. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/regex-input.html +0 -0
  166. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/settings-modal.html +0 -0
  167. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/components/theme-styles.html +0 -0
  168. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/database.html +0 -0
  169. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/goals.html +0 -0
  170. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/index.html +0 -0
  171. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/overview.html +0 -0
  172. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/search.html +0 -0
  173. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/stats.html +0 -0
  174. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/templates/utility.html +0 -0
  175. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/web/texthooking_page.py +0 -0
  176. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner/wip/__init___.py +0 -0
  177. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  178. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  179. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  180. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner.egg-info/requires.txt +0 -0
  181. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  182. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/LICENSE +0 -0
  183. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/README.md +0 -0
  184. {gamesentenceminer-2.21.25 → gamesentenceminer-2.21.29}/setup.cfg +0 -0
@@ -1,4 +1,3 @@
1
- import io
2
1
  import logging
3
2
  from PIL import Image
4
3
  import mss
@@ -7,15 +6,71 @@ from PyQt6.QtWidgets import QApplication, QWidget
7
6
  from PyQt6.QtCore import Qt, QRect
8
7
  from PyQt6.QtGui import QPainter, QPen, QColor, QPixmap, QImage
9
8
  import sys
9
+ import ctypes
10
+ import ctypes.wintypes
10
11
 
11
12
  # Import Window State Manager
12
13
  from GameSentenceMiner.ui import window_state_manager, WindowId
13
14
 
14
15
  logger = logging.getLogger("GSM_OCR")
15
16
 
17
+
18
+ def get_monitor_dpi_scale():
19
+ """
20
+ Get DPI scaling information for all monitors.
21
+ Returns a dict mapping monitor index to scale factor.
22
+ """
23
+ try:
24
+ # Get DPI awareness
25
+ user32 = ctypes.windll.user32
26
+ user32.SetProcessDPIAware()
27
+
28
+ # Enumerate all monitors and their DPI
29
+ monitors_dpi = {}
30
+
31
+ def callback(hMonitor, hdcMonitor, lprcMonitor, dwData):
32
+ try:
33
+ # Get DPI for this monitor
34
+ shcore = ctypes.windll.shcore
35
+ dpiX = ctypes.c_uint()
36
+ dpiY = ctypes.c_uint()
37
+
38
+ # MDT_EFFECTIVE_DPI = 0
39
+ shcore.GetDpiForMonitor(hMonitor, 0, ctypes.byref(dpiX), ctypes.byref(dpiY))
40
+
41
+ # Standard DPI is 96, so scale factor is dpi/96
42
+ scale = dpiX.value / 96.0
43
+
44
+ # Store by monitor count
45
+ idx = len(monitors_dpi)
46
+ monitors_dpi[idx] = scale
47
+ logger.debug(f"Monitor {idx}: DPI={dpiX.value}, Scale={scale:.2f}")
48
+ except Exception as e:
49
+ logger.warning(f"Failed to get DPI for monitor: {e}")
50
+ monitors_dpi[len(monitors_dpi)] = 1.0
51
+ return True
52
+
53
+ # Define callback type
54
+ MonitorEnumProc = ctypes.WINFUNCTYPE(
55
+ ctypes.c_bool,
56
+ ctypes.wintypes.HMONITOR,
57
+ ctypes.wintypes.HDC,
58
+ ctypes.POINTER(ctypes.wintypes.RECT),
59
+ ctypes.wintypes.LPARAM
60
+ )
61
+
62
+ # Enumerate monitors
63
+ user32.EnumDisplayMonitors(None, None, MonitorEnumProc(callback), 0)
64
+
65
+ return monitors_dpi
66
+ except Exception as e:
67
+ logger.warning(f"Failed to get monitor DPI scaling: {e}. Using 1.0")
68
+ return {}
69
+
16
70
  # Global instance
17
71
  _screen_cropper_instance = None
18
72
 
73
+
19
74
  class ScreenCropperWidget(QWidget):
20
75
  def __init__(self, parent=None):
21
76
  super().__init__(parent)
@@ -32,6 +87,11 @@ class ScreenCropperWidget(QWidget):
32
87
  self.result = None
33
88
  self.pixmap = None
34
89
 
90
+ # DPI scaling factors
91
+ self.dpi_scale_x = 1.0
92
+ self.dpi_scale_y = 1.0
93
+ self.physical_to_logical_scale = 1.0
94
+
35
95
  # Drawing state
36
96
  self.start_pos = None
37
97
  self.current_pos = None
@@ -39,9 +99,18 @@ class ScreenCropperWidget(QWidget):
39
99
 
40
100
  self.setCursor(Qt.CursorShape.CrossCursor)
41
101
 
42
- def prepare_capture(self, captured_image, monitor_geometry, main_monitor, on_complete=None, transparent_mode=False):
102
+ def prepare_capture(self, captured_image, monitor_geometry, main_monitor,
103
+ on_complete=None, transparent_mode=False, dpi_scale=1.0):
43
104
  """
44
105
  Resets the widget state for a new capture session.
106
+
107
+ Args:
108
+ captured_image: PIL Image captured at physical pixel resolution
109
+ monitor_geometry: Physical pixel coordinates from mss
110
+ main_monitor: Main monitor info from mss
111
+ on_complete: Callback function
112
+ transparent_mode: Whether to use transparent overlay mode
113
+ dpi_scale: DPI scale factor for coordinate conversion
45
114
  """
46
115
  self.captured_image = captured_image
47
116
  self.monitor_geometry = monitor_geometry
@@ -49,27 +118,46 @@ class ScreenCropperWidget(QWidget):
49
118
  self.on_complete = on_complete
50
119
  self.transparent_mode = transparent_mode
51
120
  self.result = None
121
+ self.physical_to_logical_scale = dpi_scale
52
122
 
53
123
  # Reset drawing state
54
124
  self.start_pos = None
55
125
  self.current_pos = None
56
126
  self.is_drawing = False
57
127
 
128
+ # Calculate logical coordinates for Qt widget
129
+ # mss gives us physical pixels, Qt uses logical pixels
130
+ logical_left = int(self.monitor_geometry['left'] / dpi_scale)
131
+ logical_top = int(self.monitor_geometry['top'] / dpi_scale)
132
+ logical_width = int(self.monitor_geometry['width'] / dpi_scale)
133
+ logical_height = int(self.monitor_geometry['height'] / dpi_scale)
134
+
58
135
  if not self.transparent_mode and self.captured_image:
59
- # Convert PIL Image to QPixmap
136
+ # Keep the image at full physical resolution for best quality
137
+ # Qt will handle the scaling automatically via devicePixelRatio
60
138
  img_data = self.captured_image.tobytes('raw', 'RGB')
61
- qimage = QImage(img_data, self.captured_image.width, self.captured_image.height,
62
- self.captured_image.width * 3, QImage.Format.Format_RGB888)
139
+ qimage = QImage(
140
+ img_data,
141
+ self.captured_image.width,
142
+ self.captured_image.height,
143
+ self.captured_image.width * 3,
144
+ QImage.Format.Format_RGB888
145
+ )
63
146
  self.pixmap = QPixmap.fromImage(qimage)
147
+ # Set device pixel ratio so Qt knows this is a high-DPI image
148
+ self.pixmap.setDevicePixelRatio(dpi_scale)
149
+ logger.debug(f"Pixmap size: {self.pixmap.width()}x{self.pixmap.height()}, DPR: {dpi_scale}")
64
150
  else:
65
151
  self.pixmap = None
66
152
 
67
153
  self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, self.transparent_mode)
68
154
 
69
- # Enforce geometry to match the monitor/screenshot area
70
- # We do this every time because mss coordinates are absolute
71
- self.move(self.monitor_geometry['left'], self.monitor_geometry['top'])
72
- self.resize(self.monitor_geometry['width'], self.monitor_geometry['height'])
155
+ # Set widget geometry using logical coordinates
156
+ self.move(logical_left, logical_top)
157
+ self.resize(logical_width, logical_height)
158
+
159
+ logger.info(f"Widget positioned at logical ({logical_left}, {logical_top}) "
160
+ f"size {logical_width}x{logical_height}, DPI scale: {dpi_scale:.2f}")
73
161
 
74
162
  self.show()
75
163
  self.activateWindow()
@@ -104,12 +192,11 @@ class ScreenCropperWidget(QWidget):
104
192
  else:
105
193
  # Original mode with screenshot background
106
194
  if self.pixmap:
107
- painter.drawPixmap(0, 0, self.pixmap)
195
+ # Scale the drawing to account for device pixel ratio
196
+ # The pixmap has devicePixelRatio set, so it will draw at the correct size
197
+ painter.drawPixmap(0, 0, self.width(), self.height(), self.pixmap)
108
198
 
109
- # Draw semi-transparent overlay
110
- painter.fillRect(self.rect(), QColor(0, 0, 0, 128))
111
-
112
- # If we're drawing a selection, clear that area and draw border
199
+ # If we're drawing a selection, only overlay outside the selection
113
200
  if self.start_pos and self.current_pos:
114
201
  x1 = min(self.start_pos.x(), self.current_pos.x())
115
202
  y1 = min(self.start_pos.y(), self.current_pos.y())
@@ -118,19 +205,29 @@ class ScreenCropperWidget(QWidget):
118
205
 
119
206
  selection_rect = QRect(x1, y1, x2 - x1, y2 - y1)
120
207
 
121
- # Clear the overlay in selection area
122
- painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
123
- painter.fillRect(selection_rect, Qt.GlobalColor.transparent)
124
- painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
208
+ # Draw overlay everywhere except selection area
209
+ overlay_color = QColor(0, 0, 0, 128)
125
210
 
126
- # Draw the screenshot in selection area (without overlay)
127
- if self.pixmap:
128
- painter.drawPixmap(selection_rect, self.pixmap, selection_rect)
211
+ # Top rectangle
212
+ if y1 > 0:
213
+ painter.fillRect(0, 0, self.width(), y1, overlay_color)
214
+ # Bottom rectangle
215
+ if y2 < self.height():
216
+ painter.fillRect(0, y2, self.width(), self.height() - y2, overlay_color)
217
+ # Left rectangle
218
+ if x1 > 0:
219
+ painter.fillRect(0, y1, x1, y2 - y1, overlay_color)
220
+ # Right rectangle
221
+ if x2 < self.width():
222
+ painter.fillRect(x2, y1, self.width() - x2, y2 - y1, overlay_color)
129
223
 
130
224
  # Draw red border
131
225
  pen = QPen(QColor(255, 0, 0), 3)
132
226
  painter.setPen(pen)
133
227
  painter.drawRect(selection_rect)
228
+ else:
229
+ # No selection, draw overlay over everything
230
+ painter.fillRect(self.rect(), QColor(0, 0, 0, 128))
134
231
 
135
232
  def mousePressEvent(self, event):
136
233
  if event.button() == Qt.MouseButton.LeftButton:
@@ -157,27 +254,41 @@ class ScreenCropperWidget(QWidget):
157
254
  if (x2 - x1) > 0 and (y2 - y1) > 0:
158
255
  if self.transparent_mode:
159
256
  # In transparent mode, take a fresh screenshot of the selected area
257
+ # Convert widget logical coordinates to physical screen coordinates
160
258
  try:
161
259
  with mss.mss() as sct:
162
- # Convert widget coordinates to screen coordinates
260
+ # Widget coordinates are in logical pixels, need to convert to physical
261
+ physical_x1 = int(x1 * self.physical_to_logical_scale)
262
+ physical_y1 = int(y1 * self.physical_to_logical_scale)
263
+ physical_x2 = int(x2 * self.physical_to_logical_scale)
264
+ physical_y2 = int(y2 * self.physical_to_logical_scale)
265
+
163
266
  monitor_region = {
164
- "left": self.monitor_geometry['left'] + x1,
165
- "top": self.monitor_geometry['top'] + y1,
166
- "width": x2 - x1,
167
- "height": y2 - y1
267
+ "left": self.monitor_geometry['left'] + physical_x1,
268
+ "top": self.monitor_geometry['top'] + physical_y1,
269
+ "width": physical_x2 - physical_x1,
270
+ "height": physical_y2 - physical_y1
168
271
  }
169
272
  sct_grab = sct.grab(monitor_region)
170
273
  # Convert to PIL Image
171
274
  self.result = Image.frombytes('RGB', sct_grab.size, sct_grab.bgra, 'raw', 'BGRX')
172
- logger.info(f"Fresh screenshot captured: ({monitor_region['left']}, {monitor_region['top']}) size {monitor_region['width']}x{monitor_region['height']}")
275
+ logger.info(f"Fresh screenshot captured: ({monitor_region['left']}, {monitor_region['top']}) "
276
+ f"size {monitor_region['width']}x{monitor_region['height']} (physical pixels)")
173
277
  except Exception as e:
174
278
  logger.error(f"Error capturing fresh screenshot: {e}")
175
279
  self.result = None
176
280
  else:
177
281
  # Original mode: crop from the already-captured image
282
+ # Widget coordinates are in logical pixels, need to convert to physical for cropping
178
283
  if self.captured_image:
179
- self.result = self.captured_image.crop((x1, y1, x2, y2))
180
- logger.info(f"Selection made: ({x1}, {y1}) to ({x2}, {y2})")
284
+ physical_x1 = int(x1 * self.physical_to_logical_scale)
285
+ physical_y1 = int(y1 * self.physical_to_logical_scale)
286
+ physical_x2 = int(x2 * self.physical_to_logical_scale)
287
+ physical_y2 = int(y2 * self.physical_to_logical_scale)
288
+
289
+ self.result = self.captured_image.crop((physical_x1, physical_y1, physical_x2, physical_y2))
290
+ logger.info(f"Selection made: logical ({x1}, {y1}) to ({x2}, {y2}), "
291
+ f"physical ({physical_x1}, {physical_y1}) to ({physical_x2}, {physical_y2})")
181
292
 
182
293
  # Hide instead of close to preserve instance
183
294
  self._finish()
@@ -194,12 +305,19 @@ class ScreenCropperWidget(QWidget):
194
305
  self._finish()
195
306
  elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
196
307
  # Grab main monitor area
308
+ # Convert main monitor coords from physical to logical for cropping
197
309
  if self.captured_image:
310
+ # Main monitor coordinates are in physical pixels
311
+ main_left = self.main_monitor['left'] - self.monitor_geometry['left']
312
+ main_top = self.main_monitor['top'] - self.monitor_geometry['top']
313
+ main_right = main_left + self.main_monitor['width']
314
+ main_bottom = main_top + self.main_monitor['height']
315
+
198
316
  self.result = self.captured_image.crop((
199
- self.main_monitor['left'],
200
- self.main_monitor['top'],
201
- self.main_monitor['left'] + self.main_monitor['width'],
202
- self.main_monitor['top'] + self.main_monitor['height']
317
+ main_left,
318
+ main_top,
319
+ main_right,
320
+ main_bottom
203
321
  ))
204
322
  logger.info("Main monitor area selected")
205
323
  self._finish()
@@ -225,9 +343,13 @@ def show_screen_cropper(on_complete=None, transparent_mode=False):
225
343
  """
226
344
  Displays a Qt-based screen cropper that allows the user to select a region.
227
345
  Reuses the existing widget instance.
346
+ Properly handles Windows DPI scaling.
228
347
  """
229
348
  global _screen_cropper_instance
230
349
 
350
+ # Get DPI scaling information for all monitors
351
+ monitors_dpi = get_monitor_dpi_scale()
352
+
231
353
  if not transparent_mode:
232
354
  # Original mode: capture screen first
233
355
  try:
@@ -235,19 +357,27 @@ def show_screen_cropper(on_complete=None, transparent_mode=False):
235
357
  with mss.mss() as sct:
236
358
  all_monitors_bbox = sct.monitors[0]
237
359
  main_monitor = sct.monitors[1]
360
+
361
+ # Determine DPI scale - use primary monitor's scale
362
+ # mss monitor indices start at 1 for actual monitors
363
+ dpi_scale = monitors_dpi.get(0, 1.0) # Monitor 0 in our enum is monitor 1 in mss
364
+
238
365
  monitor_geometry = {
239
366
  'left': all_monitors_bbox['left'],
240
367
  'top': all_monitors_bbox['top'],
241
368
  'width': all_monitors_bbox['width'],
242
369
  'height': all_monitors_bbox['height']
243
370
  }
371
+
372
+ # mss captures at physical pixel resolution
244
373
  sct_grab = sct.grab(all_monitors_bbox)
245
374
 
246
375
  # Convert directly from raw bytes to PIL Image (faster than to_png)
247
376
  # MSS returns BGRA format, convert to RGB
248
377
  captured_image = Image.frombytes('RGB', sct_grab.size, sct_grab.bgra, 'raw', 'BGRX')
249
378
 
250
- logger.info(f"Screen captured: {monitor_geometry['width']}x{monitor_geometry['height']}")
379
+ logger.info(f"Screen captured: {monitor_geometry['width']}x{monitor_geometry['height']} "
380
+ f"(physical pixels), DPI scale: {dpi_scale:.2f}")
251
381
  except Exception as e:
252
382
  logger.error(f"Error capturing screen: {e}")
253
383
  if on_complete:
@@ -259,6 +389,10 @@ def show_screen_cropper(on_complete=None, transparent_mode=False):
259
389
  with mss.mss() as sct:
260
390
  all_monitors_bbox = sct.monitors[0]
261
391
  main_monitor = sct.monitors[1]
392
+
393
+ # Determine DPI scale
394
+ dpi_scale = monitors_dpi.get(0, 1.0)
395
+
262
396
  monitor_geometry = {
263
397
  'left': all_monitors_bbox['left'],
264
398
  'top': all_monitors_bbox['top'],
@@ -266,7 +400,8 @@ def show_screen_cropper(on_complete=None, transparent_mode=False):
266
400
  'height': all_monitors_bbox['height']
267
401
  }
268
402
  captured_image = None
269
- logger.info(f"Transparent mode: monitor geometry {monitor_geometry['width']}x{monitor_geometry['height']}")
403
+ logger.info(f"Transparent mode: monitor geometry {monitor_geometry['width']}x{monitor_geometry['height']} "
404
+ f"(physical pixels), DPI scale: {dpi_scale:.2f}")
270
405
  except Exception as e:
271
406
  logger.error(f"Error getting monitor geometry: {e}")
272
407
  if on_complete:
@@ -283,7 +418,14 @@ def show_screen_cropper(on_complete=None, transparent_mode=False):
283
418
  _screen_cropper_instance = ScreenCropperWidget()
284
419
 
285
420
  # Prepare the widget with the new screenshot data
286
- _screen_cropper_instance.prepare_capture(captured_image, monitor_geometry, main_monitor, on_complete, transparent_mode)
421
+ _screen_cropper_instance.prepare_capture(
422
+ captured_image,
423
+ monitor_geometry,
424
+ main_monitor,
425
+ on_complete,
426
+ transparent_mode,
427
+ dpi_scale
428
+ )
287
429
 
288
430
  # Keep the widget alive by entering event loop until it's hidden
289
431
  if on_complete:
@@ -188,6 +188,13 @@ const GoalsUtils = {
188
188
  return str.charAt(0).toUpperCase() + str.slice(1);
189
189
  },
190
190
 
191
+ // Parse YYYY-MM-DD date string as local date (not UTC)
192
+ parseLocalDate(dateStr) {
193
+ if (!dateStr) return null;
194
+ const [year, month, day] = dateStr.split('-').map(Number);
195
+ return new Date(year, month - 1, day); // month is 0-indexed
196
+ },
197
+
191
198
  // Get goals from database (uses cache if available)
192
199
  async getGoalsWithFallback() {
193
200
  const data = GoalsDataManager.getCached() || await GoalsDataManager.fetchCurrent();
@@ -395,7 +402,7 @@ const CustomGoalCheckboxManager = {
395
402
 
396
403
  // Start from the most recent completion date
397
404
  let streak = 1;
398
- let prevDate = new Date(sortedDates[0]);
405
+ let prevDate = GoalsUtils.parseLocalDate(sortedDates[0]);
399
406
 
400
407
  // Count consecutive days backwards from the most recent date
401
408
  for (let i = 1; i < sortedDates.length; i++) {
@@ -477,9 +484,7 @@ const CustomGoalsManager = {
477
484
 
478
485
  // Get active goals (within current date or future)
479
486
  async getActive() {
480
- const today = new Date();
481
- today.setHours(0, 0, 0, 0);
482
- const todayStr = today.toISOString().split('T')[0];
487
+ const todayStr = GoalsUtils.getTodayDateString();
483
488
 
484
489
  const allGoals = await this.getAll();
485
490
  return allGoals.filter(goal => {
@@ -491,9 +496,7 @@ const CustomGoalsManager = {
491
496
 
492
497
  // Get goals that are currently in progress (today is within date range)
493
498
  async getInProgress() {
494
- const today = new Date();
495
- today.setHours(0, 0, 0, 0);
496
- const todayStr = today.toISOString().split('T')[0];
499
+ const todayStr = GoalsUtils.getTodayDateString();
497
500
 
498
501
  const allGoals = await this.getAll();
499
502
  return allGoals.filter(goal => {
@@ -729,15 +732,15 @@ document.addEventListener('DOMContentLoaded', function () {
729
732
 
730
733
  const progressBarClass = `completion-${Math.floor(percentage / 25) * 25}`;
731
734
 
732
- // Format dates for display
733
- const startDate = new Date(goal.startDate);
734
- const endDate = new Date(goal.endDate);
735
+ // Format dates for display - parse YYYY-MM-DD as local date
736
+ const startDate = GoalsUtils.parseLocalDate(goal.startDate);
737
+ const endDate = GoalsUtils.parseLocalDate(goal.endDate);
735
738
  let formattedStartDate = 'N/A';
736
739
 
737
- if (startDate >= new Date(1980, 0, 1)) {
740
+ if (startDate && startDate >= new Date(1980, 0, 1)) {
738
741
  formattedStartDate = startDate.toLocaleDateString(navigator.language, { month: 'short', day: 'numeric', year: 'numeric' });
739
742
  }
740
- const formattedEndDate = endDate.toLocaleDateString(navigator.language, { month: 'short', day: 'numeric', year: 'numeric' });
743
+ const formattedEndDate = endDate ? endDate.toLocaleDateString(navigator.language, { month: 'short', day: 'numeric', year: 'numeric' }) : 'N/A';
741
744
 
742
745
  return `
743
746
  <div class="goal-progress-item custom-goal-item" data-goal-id="${goal.id}">
@@ -1148,9 +1151,9 @@ document.addEventListener('DOMContentLoaded', function () {
1148
1151
  formattedProjection = projectionData.projection.toLocaleString();
1149
1152
  }
1150
1153
 
1151
- // Format target date
1152
- const targetDate = new Date(projectionData.end_date);
1153
- const formattedTargetDate = targetDate.toLocaleDateString(navigator.language);
1154
+ // Format target date - parse YYYY-MM-DD as local date
1155
+ const targetDate = GoalsUtils.parseLocalDate(projectionData.end_date);
1156
+ const formattedTargetDate = targetDate ? targetDate.toLocaleDateString(navigator.language) : 'N/A';
1154
1157
 
1155
1158
  // Calculate projected completion date
1156
1159
  const remaining = Math.max(0, projectionData.target - projectionData.current);
@@ -1231,9 +1234,7 @@ document.addEventListener('DOMContentLoaded', function () {
1231
1234
 
1232
1235
  // Filter for only the 5 core metrics and goals that have started
1233
1236
  const coreMetrics = ['hours', 'characters', 'games', 'cards', 'mature_cards'];
1234
- const today = new Date();
1235
- today.setHours(0, 0, 0, 0);
1236
- const todayStr = today.toISOString().split('T')[0];
1237
+ const todayStr = GoalsUtils.getTodayDateString();
1237
1238
 
1238
1239
  const customGoalsWithProjections = customGoals.filter(goal => {
1239
1240
  // Must be a core metric
@@ -1387,8 +1388,8 @@ document.addEventListener('DOMContentLoaded', function () {
1387
1388
  // Helper to calculate days between two dates (inclusive)
1388
1389
  function getDaysBetween(start, end) {
1389
1390
  if (!start || !end) return null;
1390
- const startDate = new Date(start);
1391
- const endDate = new Date(end);
1391
+ const startDate = GoalsUtils.parseLocalDate(start);
1392
+ const endDate = GoalsUtils.parseLocalDate(end);
1392
1393
  if (isNaN(startDate) || isNaN(endDate)) return null;
1393
1394
  // Add 1 to include both start and end dates
1394
1395
  return Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.21.25
3
+ Version: 2.21.29
4
4
  Summary: A tool for mining sentences from games. Update: Dependencies, replay buffer based line searching, and bug fixes.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.21.25
3
+ Version: 2.21.29
4
4
  Summary: A tool for mining sentences from games. Update: Dependencies, replay buffer based line searching, and bug fixes.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GameSentenceMiner"
10
- version = "2.21.25"
10
+ version = "2.21.29"
11
11
  description = "A tool for mining sentences from games. Update: Dependencies, replay buffer based line searching, and bug fixes."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10, <3.14"