From ac21bbbfbaa0e56244bf72d8cd1dd81334059c38 Mon Sep 17 00:00:00 2001 From: Mathew Chan Date: Sun, 3 Jul 2022 17:12:34 +0800 Subject: [PATCH] add word audio, pitch, and clipboard functionaltiy --- requirements/base.txt | 1 + src/main/python/anki/anki_bridge.py | 174 ----------------- src/main/python/anki/anki_connect.py | 22 ++- src/main/python/anki/anki_settings.py | 2 + src/main/python/call_handler.py | 6 + src/main/python/config.py | 23 +++ src/main/python/control_panel.py | 176 ++++++++++++++++++ src/main/python/game2text/__init__.py | 2 +- .../python/{ => game2text}/web_overlay.py | 4 +- src/main/python/main.py | 150 +-------------- .../{tests => sandbox}/japanese_test.py | 0 src/main/python/sandbox/web_overlay.py | 125 +++++++++++++ .../{tests => sandbox}/web_overlay_test.py | 25 ++- src/main/python/ui/main_ui.py | 63 +++++-- src/main/python/util/image/image_object.py | 10 +- src/main/python/{anki => util}/word_audio.py | 22 ++- src/main/resources/base/config.ini | 5 + src/main/resources/base/rikaisama/data.js | 3 +- .../resources/base/rikaisama/raikaichan.js | 59 ++++-- 19 files changed, 500 insertions(+), 372 deletions(-) delete mode 100644 src/main/python/anki/anki_bridge.py create mode 100644 src/main/python/config.py create mode 100644 src/main/python/control_panel.py rename src/main/python/{ => game2text}/web_overlay.py (98%) rename src/main/python/{tests => sandbox}/japanese_test.py (100%) create mode 100644 src/main/python/sandbox/web_overlay.py rename src/main/python/{tests => sandbox}/web_overlay_test.py (75%) rename src/main/python/{anki => util}/word_audio.py (57%) create mode 100644 src/main/resources/base/config.ini diff --git a/requirements/base.txt b/requirements/base.txt index 26962a0..c7cd2b8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,4 +10,5 @@ Pillow==8.4.0 PyYAML==6.0 pyinstaller==4.10 pynput==1.7.6 +pyperclip==1.8.2 QtAwesome==1.1.1 \ No newline at end of file diff --git a/src/main/python/anki/anki_bridge.py b/src/main/python/anki/anki_bridge.py deleted file mode 100644 index 50b75c3..0000000 --- a/src/main/python/anki/anki_bridge.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2013 Alex Yatskov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import anki -import aqt -import hashlib -import urllib2 - - -# -# Audio helpers -# - -def audioBuildFilename(kana, kanji): - filename = u'yomichan_{}'.format(kana) - if kanji: - filename += u'_{}'.format(kanji) - filename += u'.mp3' - return filename - - -def audioDownload(kana, kanji): - url = 'http://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji={}'.format(urllib2.quote(kanji.encode('utf-8'))) - if kana: - url += '&kana={}'.format(urllib2.quote(kana.encode('utf-8'))) - - try: - resp = urllib2.urlopen(url) - except urllib2.URLError: - return None - - if resp.code != 200: - return None - - return resp.read() - - -def audioIsPlaceholder(data): - m = hashlib.md5() - m.update(data) - return m.hexdigest() == '7e2c2f954ef6051373ba916f000168dc' - - -def audioInject(note, fields, filename): - for field in fields: - if field in note: - note[field] += u'[sound:{}]'.format(filename) - - -# -# Anki -# - -class Anki: - def addNote(self, deckName, modelName, fields, tags, audio): - collection = self.collection() - if collection is None: - return - - note = self.createNote(deckName, modelName, fields, tags) - if note is None: - return - - if audio is not None and len(audio['fields']) > 0: - data = audioDownload(audio['kana'], audio['kanji']) - if data is not None and not audioIsPlaceholder(data): - filename = audioBuildFilename(audio['kana'], audio['kanji']) - audioInject(note, audio['fields'], filename) - self.media().writeData(filename, data) - - self.startEditing() - collection.addNote(note) - collection.autosave() - - return note.id - - - def canAddNote(self, deckName, modelName, fields): - return bool(self.createNote(deckName, modelName, fields)) - - - def createNote(self, deckName, modelName, fields, tags=[]): - collection = self.collection() - if collection is None: - return - - model = collection.models.byName(modelName) - if model is None: - return - - deck = collection.decks.byName(deckName) - if deck is None: - return - - note = anki.notes.Note(collection, model) - note.model()['did'] = deck['id'] - note.tags = tags - - for name, value in fields.items(): - if name in note: - note[name] = value - - if not note.dupeOrEmpty(): - return note - - - def browseNote(self, noteId): - browser = aqt.dialogs.open('Browser', self.window()) - browser.form.searchEdit.lineEdit().setText('nid:{0}'.format(noteId)) - browser.onSearch() - - - def startEditing(self): - self.window().requireReset() - - - def stopEditing(self): - if self.collection() is not None: - self.window().maybeReset() - - - def window(self): - return aqt.mw - - - def addUiAction(self, action): - self.window().form.menuTools.addAction(action) - - - def collection(self): - return self.window().col - - - def media(self): - collection = self.collection() - if collection is not None: - return collection.media - - - def modelNames(self): - collection = self.collection() - if collection is not None: - return collection.models.allNames() - - - def modelFieldNames(self, modelName): - collection = self.collection() - if collection is None: - return - - model = collection.models.byName(modelName) - if model is not None: - return [field['name'] for field in model['flds']] - - - def deckNames(self): - collection = self.collection() - if collection is not None: - return collection.decks.allNames() \ No newline at end of file diff --git a/src/main/python/anki/anki_connect.py b/src/main/python/anki/anki_connect.py index 33f5fe9..2d85895 100644 --- a/src/main/python/anki/anki_connect.py +++ b/src/main/python/anki/anki_connect.py @@ -2,10 +2,9 @@ import logging import urllib.request import time -import yaml from threading import Thread from .anki_model import AnkiModel -# from .word_audio import get_jpod_audio_base64' +from util.word_audio import get_jpod_audio_base64 def request(action, params): return {'action': action, 'params': params, 'version': 6} @@ -102,17 +101,28 @@ def set_deck(self, deck_name): def create_anki_note(self, note_data): if not self.anki_settings: + logging.error("Anki settings not configured") return field_value_map = self.anki_settings.get_field_value_map(self.model) if not field_value_map: + logging.error("Anki field value map not configured") return fields = {} screenshot_field = None + word_audio_field = None for field, value in field_value_map.items(): if value.lower() == 'screenshot': if 'screenshot' in note_data: screenshot_field = field + elif value.lower() == 'word_audio': + if 'expression' in note_data and 'reading' in note_data: + note_data['word_audio'] = get_jpod_audio_base64(note_data['expression'], note_data['reading']) + word_audio_field = field + elif value.lower() == 'pitch': + if 'expression' in note_data and 'reading' in note_data: + if self.anki_settings.pitch: + fields[field] = self.anki_settings.pitch.get_pitch(note_data['expression'], note_data['reading']) else: fields[field] = note_data[value.lower()] @@ -132,6 +142,14 @@ def create_anki_note(self, note_data): screenshot_field ] }] + if word_audio_field: + note['audio'] = [{ + "data": note_data['word_audio'], + "filename": '_{}.mp3'.format(time.time()), + "fields": [ + word_audio_field + ] + }] result = self.invoke('addNote', note=note) return result diff --git a/src/main/python/anki/anki_settings.py b/src/main/python/anki/anki_settings.py index b0160d0..6d83256 100644 --- a/src/main/python/anki/anki_settings.py +++ b/src/main/python/anki/anki_settings.py @@ -1,4 +1,5 @@ import yaml +from japanese.pitch import Pitch class AnkiSettings(): def __init__(self, appctxt): @@ -6,6 +7,7 @@ def __init__(self, appctxt): self.anki_defaults_path = appctxt.get_resource('anki/user_defaults.yaml') self.active_model = None self.active_field_value_map = None + self.pitch = Pitch(appctxt.get_resource('rikaisama/pitch_accents.sqlite')) def get_default_deck_model(self): with open(self.anki_defaults_path, 'r') as stream: diff --git a/src/main/python/call_handler.py b/src/main/python/call_handler.py index bf1e973..29c6b77 100644 --- a/src/main/python/call_handler.py +++ b/src/main/python/call_handler.py @@ -1,7 +1,9 @@ from PyQt5.QtCore import QObject, pyqtSlot, QVariant from japanese.pitch import Pitch +from util.word_audio import get_jpod_audio_url import json +import pyperclip # Bridge between PyQt and Web class CallHandler(QObject): @@ -23,6 +25,10 @@ def send_to_anki(self, args): print('created') print(result) + @pyqtSlot(QVariant) + def copy_to_clipboard(self, text): + pyperclip.copy(text) + @pyqtSlot(QVariant, result=str) def get_pitch(self, args): pitch_dictionary = Pitch(self.appctxt.get_resource('rikaisama/pitch_accents.sqlite')) diff --git a/src/main/python/config.py b/src/main/python/config.py new file mode 100644 index 0000000..2076f92 --- /dev/null +++ b/src/main/python/config.py @@ -0,0 +1,23 @@ +from configparser import ConfigParser + +class Config(): + def __init__(self, appctxt): + self.config_file = appctxt.get_resource('config.ini') + self.config_object = ConfigParser() + + def read(self, section_name, key): + self.config_object.read(self.config_file, encoding='utf-8') + section = self.config_object[section_name] + return section[key] + + def write(self, section_name, to_update_dict): + self.config_object.read(self.config_file, encoding='utf-8') + section = self.config_object[section_name] + + # Update the key value + for key, value in to_update_dict.items(): + section[key] = value + + # Write changes back to file + with open(self.config_file, 'w', encoding='utf-8') as conf: + self.config_object.write(conf) \ No newline at end of file diff --git a/src/main/python/control_panel.py b/src/main/python/control_panel.py new file mode 100644 index 0000000..0a0fafd --- /dev/null +++ b/src/main/python/control_panel.py @@ -0,0 +1,176 @@ +import logging +from PyQt5.QtWidgets import QWidget +from screenshot.capture_window import CaptureWindow +from screenshot.capture_screen import CaptureScreen +from screenshot import Capture_Mode +from ui.main_ui import UIMain +from game2text import Game2Text + +class ControlPanel(QWidget, UIMain): + def __init__(self, parent): + QWidget.__init__(self, parent) + self.setupUi(self) + + # Window Capture + self.windows = [] + self.selected_window = None + self.capture_window = CaptureWindow() + + # Area Capture + self.snipping_widget = CaptureScreen() + self.snipping_widget.onSnippingCompleted = self.on_snipping_completed + self.snipped_capture = None + + self.game2text = Game2Text(parent.ocr, parent.capture, parent.call_handler) + self.running_ocr = False + self.capture_mode = Capture_Mode.WINDOW + + self.captureComboBox.currentIndexChanged.connect(self.select_capture_mode) + self.captureWindowComboBox.activated.connect(self.select_window) + self.deckComboBox.currentIndexChanged.connect(self.on_deck_change) + self.deckComboBox.activated.connect(self.select_deck) + self.modelComboBox.currentIndexChanged.connect(self.on_model_change) + self.modelComboBox.activated.connect(self.select_model) + self.selectRegionButton.clicked.connect(self.select_area) + self.start_button.clicked.connect(self.toggle_ocr) + self.reloadAnkiButton.clicked.connect(parent.load_anki) + + self.resizeWidthInput.textChanged.connect(self.set_resize_width) + self.resizeHeightInput.textChanged.connect(self.set_resize_height) + self.resizeCheckBox.stateChanged.connect(self.toggle_resize) + + # Anki Settings + self.anki_connect = parent.anki_connect + self.anki_settings = parent.anki_settings + self.config = parent.config + self.decks = [] + self.models = [] + self.deck_combo_ready = False + self.model_combo_ready = False + self.selected_model = None + self.tableFields.on_change = self.on_anki_options_update + + def on_deck_change(self, index): + if not self.deck_combo_ready: + deck, model = self.anki_settings.get_default_deck_model() + if deck and deck in self.decks: + index = self.decks.index(deck) + self.deckComboBox.setCurrentIndex(index) + self.select_deck(index, persist=False) + else: + self.deckComboBox.setCurrentIndex(-1) + self.deck_combo_ready = True + + def select_deck(self, index, persist=True): + if self.decks: + deck = self.decks[index] + self.anki_connect.set_deck(deck) + if persist: + self.anki_settings.update_default_deck(deck) + + def on_model_change(self, index): + if not self.model_combo_ready: + deck, model = self.anki_settings.get_default_deck_model() + model_names = [model.model_name for model in self.models] + if model and model in model_names: + index = model_names.index(model) + self.modelComboBox.setCurrentIndex(index) + self.select_model(index, persist=False) + else: + self.modelComboBox.setCurrentIndex(-1) + self.model_combo_ready = True + + def select_model(self, index, persist=True): + if self.models: + self.selected_model = self.models[index] + fields = self.selected_model.fields + self.tableFields.setRowCount(len(fields)) + field_value_map = self.anki_settings.get_field_value_map(self.selected_model.model_name) + self.tableFields.setData(fields, field_value_map) + self.tableFields.show() + self.anki_connect.set_model(self.selected_model.model_name) + if persist: + self.anki_settings.update_default_model(self.selected_model.model_name) + + def set_decks(self, decks): + self.decks = decks + self.deckComboBox.addItems(decks) + + def set_models(self, models): + self.models = models + self.modelComboBox.addItems([model.model_name for model in self.models]) + + def set_windows(self, windows): + self.windows = windows + self.captureWindowComboBox.clear() + new_selected_index = -1 + for index, window in enumerate(windows): + self.captureWindowComboBox.addItem(window) + if window == self.selected_window: + new_selected_index = index + self.captureWindowComboBox.setCurrentIndex(new_selected_index) + + def select_capture_mode(self, index): + self.capture_mode = Capture_Mode(index) + + def select_window(self, index): + if self.windows: + self.start_button.setEnabled(True) + self.selected_window = self.windows[index] + self.capture_window.setWindowTitle(self.selected_window) + + def get_capture(self): + if self.capture_mode == Capture_Mode.WINDOW: + return self.capture_window.get_capture() + elif self.capture_mode == Capture_Mode.DESKTOP_AREA: + return self.get_area_capture() + else: + return None + + def select_area(self): + self.game2text.stop() + self.snipping_widget.start() + + def get_area_capture(self): + return self.snipping_widget.captureArea() + + def on_snipping_completed(self, capture_object): + if capture_object.is_valid(): + self.snipped_capture = capture_object + self.regionInfoLabel.setText(capture_object.get_region_info()) + self.start_button.setEnabled(True) + + def toggle_ocr(self): + if self.game2text: + if self.running_ocr: + self.game2text.stop() + else: + self.game2text.run() + self.running_ocr = not self.running_ocr + + def on_anki_options_update(self, user_field_map): + self.anki_settings.update_user_model(self.selected_model.model_name, user_field_map) + + def toggle_resize(self, int): + if self.resizeCheckBox.isChecked(): + self.config.write('IMAGECONFIG', {'resize_screenshot': 'true'}) + else: + self.config.write('IMAGECONFIG', {'resize_screenshot': 'false'}) + + def set_resize_width(self, text): + try: + width = int(text) + except: + logging.error("width not a number") + finally: + if width and width > 0: + self.config.write('IMAGECONFIG', {'resize_screenshot_max_width': text}) + + def set_resize_height(self, text): + try: + height = int(text) + except: + logging.error("width not a number") + finally: + if height and height > 0: + self.config.write('IMAGECONFIG', {'resize_screenshot_max_height': text}) diff --git a/src/main/python/game2text/__init__.py b/src/main/python/game2text/__init__.py index 2777f00..c647956 100644 --- a/src/main/python/game2text/__init__.py +++ b/src/main/python/game2text/__init__.py @@ -2,8 +2,8 @@ from util.box import box_to_qt from util.cursor import cursor_position from util.detection_box import grouped_boxes -from web_overlay import WebOverlay from pyqt_custom.blur_window import BlurWindow +from .web_overlay import WebOverlay from .workers.ocr_worker import OcrWorker from .workers.capture_worker import CaptureWorker diff --git a/src/main/python/web_overlay.py b/src/main/python/game2text/web_overlay.py similarity index 98% rename from src/main/python/web_overlay.py rename to src/main/python/game2text/web_overlay.py index 4271aae..8eade0d 100644 --- a/src/main/python/web_overlay.py +++ b/src/main/python/game2text/web_overlay.py @@ -1,10 +1,8 @@ -import sys, os -import json +import sys from fbs_runtime.application_context.PyQt5 import ApplicationContext from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWebChannel import QWebChannel from PyQt5.QtCore import QUrl, Qt -from call_handler import CallHandler sys.argv.append("--disable-web-security") diff --git a/src/main/python/main.py b/src/main/python/main.py index 1f41ad4..2475056 100644 --- a/src/main/python/main.py +++ b/src/main/python/main.py @@ -1,17 +1,14 @@ import sys from fbs_runtime.application_context.PyQt5 import ApplicationContext -from PyQt5.QtWidgets import QMainWindow, QWidget +from PyQt5.QtWidgets import QMainWindow from anki.anki_connect import AnkiConnect from anki.anki_settings import AnkiSettings from call_handler import CallHandler -from screenshot.capture_window import CaptureWindow -from screenshot.capture_screen import CaptureScreen -from screenshot import Capture_Mode from screenshot.hwnd_worker import HWNDWorker from threading import Thread -from ui.main_ui import UIMain from game2text.ocr import OCR -from game2text import Game2Text +from config import Config +from control_panel import ControlPanel class Main(QMainWindow): def __init__(self, appctxt): @@ -23,6 +20,7 @@ def __init__(self, appctxt): self.anki_connect = AnkiConnect(anki_settings=self.anki_settings) self.ocr = OCR(appctxt) self.call_handler = CallHandler(appctxt, self.anki_connect) + self.config = Config(appctxt) self.control_panel = ControlPanel(self) self.setCentralWidget(self.control_panel) @@ -54,146 +52,6 @@ def load_anki(self): model_thread.start() deck_thread.start() -class ControlPanel(QWidget, UIMain): - def __init__(self, parent): - QWidget.__init__(self, parent) - self.setupUi(self) - - # Window Capture - self.windows = [] - self.selected_window = None - self.capture_window = CaptureWindow() - - # Area Capture - self.snipping_widget = CaptureScreen() - self.snipping_widget.onSnippingCompleted = self.on_snipping_completed - self.snipped_capture = None - - self.game2text = Game2Text(parent.ocr, parent.capture, parent.call_handler) - self.running_ocr = False - self.capture_mode = Capture_Mode.WINDOW - - self.captureComboBox.currentIndexChanged.connect(self.select_capture_mode) - self.captureWindowComboBox.activated.connect(self.select_window) - self.deckComboBox.currentIndexChanged.connect(self.on_deck_change) - self.deckComboBox.activated.connect(self.select_deck) - self.modelComboBox.currentIndexChanged.connect(self.on_model_change) - self.modelComboBox.activated.connect(self.select_model) - self.selectRegionButton.clicked.connect(self.select_area) - self.start_button.clicked.connect(self.toggle_ocr) - self.reloadAnkiButton.clicked.connect(parent.load_anki) - - # Anki Settings - self.anki_connect = parent.anki_connect - self.anki_settings = parent.anki_settings - self.decks = [] - self.models = [] - self.deck_combo_ready = False - self.model_combo_ready = False - self.selected_model = None - self.tableFields.on_change = self.on_anki_options_update - - def on_deck_change(self, index): - if not self.deck_combo_ready: - deck, model = self.anki_settings.get_default_deck_model() - if deck and deck in self.decks: - index = self.decks.index(deck) - self.deckComboBox.setCurrentIndex(index) - self.select_deck(index, persist=False) - else: - self.deckComboBox.setCurrentIndex(-1) - self.deck_combo_ready = True - - def select_deck(self, index, persist=False): - if self.decks: - deck = self.decks[index] - self.anki_connect.set_deck(deck) - if persist: - self.anki_settings.update_default_deck(deck) - - def on_model_change(self, index): - if not self.model_combo_ready: - deck, model = self.anki_settings.get_default_deck_model() - model_names = [model.model_name for model in self.models] - if model and model in model_names: - index = model_names.index(model) - self.modelComboBox.setCurrentIndex(index) - self.select_model(index, persist=False) - else: - self.modelComboBox.setCurrentIndex(-1) - self.model_combo_ready = True - - def select_model(self, index, persist=True): - if self.models: - self.selected_model = self.models[index] - fields = self.selected_model.fields - self.tableFields.setRowCount(len(fields)) - field_value_map = self.anki_settings.get_field_value_map(self.selected_model.model_name) - self.tableFields.setData(fields, field_value_map) - self.tableFields.show() - self.anki_connect.set_model(self.selected_model.model_name) - if persist: - self.anki_settings.update_default_model(self.selected_model.model_name) - - def set_decks(self, decks): - self.decks = decks - self.deckComboBox.addItems(decks) - - def set_models(self, models): - self.models = models - self.modelComboBox.addItems([model.model_name for model in self.models]) - - def set_windows(self, windows): - self.windows = windows - self.captureWindowComboBox.clear() - new_selected_index = -1 - for index, window in enumerate(windows): - self.captureWindowComboBox.addItem(window) - if window == self.selected_window: - new_selected_index = index - self.captureWindowComboBox.setCurrentIndex(new_selected_index) - - def select_capture_mode(self, index): - self.capture_mode = Capture_Mode(index) - - def select_window(self, index): - if self.windows: - self.start_button.setEnabled(True) - self.selected_window = self.windows[index] - self.capture_window.setWindowTitle(self.selected_window) - - def get_capture(self): - if self.capture_mode == Capture_Mode.WINDOW: - return self.capture_window.get_capture() - elif self.capture_mode == Capture_Mode.DESKTOP_AREA: - return self.get_area_capture() - else: - return None - - def select_area(self): - self.game2text.stop() - self.snipping_widget.start() - - def get_area_capture(self): - return self.snipping_widget.captureArea() - - def on_snipping_completed(self, capture_object): - if capture_object.is_valid(): - self.snipped_capture = capture_object - self.regionInfoLabel.setText(capture_object.get_region_info()) - self.start_button.setEnabled(True) - - def toggle_ocr(self): - if self.game2text: - if self.running_ocr: - self.game2text.stop() - else: - self.game2text.run() - self.running_ocr = not self.running_ocr - - def on_anki_options_update(self, user_field_map): - self.anki_settings.update_user_model(self.selected_model.model_name, user_field_map) - def main(): appctxt = ApplicationContext() window = Main(appctxt) diff --git a/src/main/python/tests/japanese_test.py b/src/main/python/sandbox/japanese_test.py similarity index 100% rename from src/main/python/tests/japanese_test.py rename to src/main/python/sandbox/japanese_test.py diff --git a/src/main/python/sandbox/web_overlay.py b/src/main/python/sandbox/web_overlay.py new file mode 100644 index 0000000..8eade0d --- /dev/null +++ b/src/main/python/sandbox/web_overlay.py @@ -0,0 +1,125 @@ +import sys +from fbs_runtime.application_context.PyQt5 import ApplicationContext +from PyQt5.QtWebEngineWidgets import QWebEngineView +from PyQt5.QtWebChannel import QWebChannel +from PyQt5.QtCore import QUrl, Qt + +sys.argv.append("--disable-web-security") + +RIKAISAMA_JS_LIST = ['radicals.js', 'kanji.js', 'jedict.js', 'deinflect.js', 'data.js', 'config.js', 'options.js', 'raikaichan.js'] +FONT_REGULAR = 'web/fonts/M_PLUS_1p/MPLUS1p-Regular.ttf' +FONT_BOLD = 'web/fonts/M_PLUS_1p/MPLUS1p-Medium.ttf' + +appctxt = ApplicationContext() + +def getResourceUrl(filename): + return QUrl.fromLocalFile(appctxt.get_resource(filename)).toString() + +class WebOverlay(QWebEngineView): + ready = False + containers = 0 + + def __init__(self, x=0, y=0, w=800, h=300, handler=None): + super(WebOverlay, self).__init__() + + # Web channel + self.channel = QWebChannel() + self.handler = handler + self.channel.registerObject('handler', self.handler) + self.page().setWebChannel(self.channel) + + # Window Attributes + self.setGeometry(x, y, w, h) + self.resize(w, h) + # self.resize_fixed(w, h) + + self.setWindowFlags( + Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint + ) + + self.setAttribute(Qt.WA_NoSystemBackground, True) + self.setAttribute(Qt.WA_TranslucentBackground, True) + self.page().setBackgroundColor(Qt.transparent) + self.load_html() + + # def resize_fixed(self, w, h): + # self.setFixedWidth(w) + # self.setFixedHeight(h) + # self.w = w + # self.h = h + + def setReady(self, ready): + self.ready = ready + print('ready!') + + def load_html(self): + raw_html = '' + raw_html += '' + raw_html += ''' + + '''.format(getResourceUrl(FONT_REGULAR), getResourceUrl(FONT_BOLD)) + raw_html += ''.format(getResourceUrl("web/scale.css")) + raw_html += ''.format(getResourceUrl("rikaisama/popup-blue.css")) + raw_html += ''.format(getResourceUrl("web/scale.js")) + raw_html += '' + raw_html += '
' + for js_file in RIKAISAMA_JS_LIST: + raw_html += ''.format(getResourceUrl('rikaisama/' + js_file)) + raw_html += ''' + + ''' + raw_html += '' + self.setHtml(raw_html, baseUrl=QUrl.fromLocalFile(appctxt.get_resource("web"))) + self.loadFinished.connect(lambda x: self.setReady(True)) + + def setScreenshot(self, screenshot): + self.handler.setScreenshot(screenshot) + + def updateText(self, detection_box): + if not self.ready: + self.loadFinished.connect(lambda x: self.updateText(detection_box)) + return + text_boxes = detection_box.text_boxes + script = 'var templateContainer = document.getElementById("container-template");' + # clear old textboxes + for existing_index in range(self.containers): + script += 'document.body.removeChild(document.getElementById("container-{}"));'.format(existing_index) + # add new textboxes + for index, text_box in enumerate(text_boxes): + text = text_box.text + postText = '' if (index >= len(text_boxes)-1) else '
' + x, y, x2, y2 = text_box.box + w = text_box.width() + h = text_box.height() + script += 'var containerClone = templateContainer.cloneNode(templateContainer);' + script += 'containerClone.id = "container-{}";'.format(index) + script += 'containerClone.style.width = "{}px";'.format(w) + script += 'containerClone.style.height = "{}px";'.format(h) + script += 'containerClone.style.top = "{}px";'.format(y) + script += 'containerClone.style.left = "{}px";'.format(x) + script += 'var textElement = document.createElement("div");textElement.className = "scale--js";' + script += 'textElement.innerHTML = "{}";'.format(text+postText) + script += 'containerClone.appendChild(textElement);' + script += 'containerClone.hidden = false;' + script += 'document.body.appendChild(containerClone);' + script += 'myScaleFunction();' + self.page().runJavaScript(script) + self.containers = len(text_boxes) \ No newline at end of file diff --git a/src/main/python/tests/web_overlay_test.py b/src/main/python/sandbox/web_overlay_test.py similarity index 75% rename from src/main/python/tests/web_overlay_test.py rename to src/main/python/sandbox/web_overlay_test.py index 494b161..8b95b67 100644 --- a/src/main/python/tests/web_overlay_test.py +++ b/src/main/python/sandbox/web_overlay_test.py @@ -1,13 +1,17 @@ from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import Qt import sys, os +from web_overlay import WebOverlay +from PyQt5.QtCore import QObject, pyqtSlot, QVariant +import json + SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.dirname(SCRIPT_DIR)) -from web_overlay import WebOverlay class WebOverlayTest(): def __init__(self): - self.web_overlay = WebOverlay(0, 0, 550, 300) + self.call_handler = CallHandler() + self.web_overlay = WebOverlay(0, 0, 550, 300, self.call_handler) self.web_overlay.setWindowFlags(Qt.WindowStaysOnTopHint) self.web_overlay.setAttribute(Qt.WA_NoSystemBackground, False) self.web_overlay.setAttribute(Qt.WA_TranslucentBackground, False) @@ -32,6 +36,23 @@ def testText(self, text, x=0, y=0, w=300, h=50): script += 'myScaleFunction();' self.web_overlay.page().runJavaScript(script) +import pyperclip + +class CallHandler(QObject): + def __init__(self): + super().__init__() + + @pyqtSlot(QVariant) + def send_to_anki(self, args): + vocab = json.loads(args) + print(vocab) + @pyqtSlot(QVariant, result=str) + def get_pitch(self, args): + return '' + @pyqtSlot(QVariant) + def copy_to_clipboard(self, text): + pyperclip.copy(text) + if __name__ == "__main__": app = QApplication(sys.argv) test = WebOverlayTest() diff --git a/src/main/python/ui/main_ui.py b/src/main/python/ui/main_ui.py index 981b757..a8ddcff 100644 --- a/src/main/python/ui/main_ui.py +++ b/src/main/python/ui/main_ui.py @@ -1,5 +1,6 @@ -from PyQt5.QtWidgets import QGridLayout, QLabel, QComboBox, QPushButton, QWidget, QTabWidget,QVBoxLayout +from PyQt5.QtWidgets import QGridLayout, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QComboBox, QPushButton, QWidget, QTabWidget,QVBoxLayout from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIntValidator from settings.anki_field_table import AnkiFieldTable from screenshot import Capture_Mode @@ -12,11 +13,13 @@ def setupUi(self, parent): self.tabs = QTabWidget() self.tab1 = QWidget() self.tab2 = QWidget() + self.settingsTab = QWidget() self.tabs.resize(300,200) # Add tabs - self.tabs.addTab(self.tab2,"OCR") - self.tabs.addTab(self.tab1,"Anki") + self.tabs.addTab(self.tab1,"OCR") + self.tabs.addTab(self.tab2,"Anki") + self.tabs.addTab(self.settingsTab,"Settings") # Capture settings self.captureLayout = QGridLayout() @@ -46,14 +49,14 @@ def setupUi(self, parent): self.start_button.setCheckable(True) self.start_button.clicked.connect(self.changeColor) - # Create second tab - self.tab2.layout = QVBoxLayout() - self.tab2.layout.addLayout(self.captureLayout) - self.tab2.layout.addWidget(self.selectRegionButton) - self.tab2.layout.addWidget(self.start_button) - self.tab2.layout.addWidget(self.regionInfoLabel) - self.tab2.setLayout(self.tab2.layout) - self.tab2.layout.setAlignment(Qt.AlignTop) + # Create first tab + self.tab1.layout = QVBoxLayout() + self.tab1.layout.addLayout(self.captureLayout) + self.tab1.layout.addWidget(self.selectRegionButton) + self.tab1.layout.addWidget(self.start_button) + self.tab1.layout.addWidget(self.regionInfoLabel) + self.tab1.setLayout(self.tab1.layout) + self.tab1.layout.setAlignment(Qt.AlignTop) # Deck and model row self.modelDeckLayout = QGridLayout() @@ -73,14 +76,36 @@ def setupUi(self, parent): # reload anki button self.reloadAnkiButton = QPushButton("Reload") - # First Tab - self.tab1.layout = QVBoxLayout() - self.tab1.layout.addLayout(self.modelDeckLayout) - self.tab1.layout.addWidget(self.tableFields) - self.tab1.layout.addWidget(self.reloadAnkiButton) - self.tab1.layout.addWidget(QLabel('Make sure AnkiConnect is installed, and Anki is open.')) - self.tab1.setLayout(self.tab1.layout) - + # Second Tab + self.tab2.layout = QVBoxLayout() + self.tab2.layout.addLayout(self.modelDeckLayout) + self.tab2.layout.addWidget(self.tableFields) + self.tab2.layout.addWidget(self.reloadAnkiButton) + self.tab2.layout.addWidget(QLabel('Make sure AnkiConnect is installed, and Anki is open.')) + self.tab2.setLayout(self.tab2.layout) + + # Settings + self.resizeInputRow = QHBoxLayout() + self.resizeCheckBox = QCheckBox("Resize Screenshot") + self.resizeCheckBox.setChecked(True) + self.resizeInputRow.addWidget(QLabel('Width')) + self.resizeWidthInput = QLineEdit() + self.resizeWidthInput.setValidator(QIntValidator()) + self.resizeWidthInput.setText('1280') + self.resizeInputRow.addWidget(self.resizeWidthInput) + self.resizeInputRow.addWidget(QLabel('Height')) + self.resizeHeightInput = QLineEdit() + self.resizeHeightInput.setValidator(QIntValidator()) + self.resizeHeightInput.setText('720') + self.resizeInputRow.addWidget(self.resizeHeightInput) + + # Third Tab + self.settingsTab.layout = QVBoxLayout() + self.settingsTab.layout.addWidget(self.resizeCheckBox) + self.settingsTab.layout.addLayout(self.resizeInputRow) + self.settingsTab.setLayout(self.settingsTab.layout) + self.settingsTab.layout.setAlignment(Qt.AlignTop) + # Add tabs to widget self.layout.addWidget(self.tabs) self.setLayout(self.layout) diff --git a/src/main/python/util/image/image_object.py b/src/main/python/util/image/image_object.py index b1705f3..a2c39e7 100644 --- a/src/main/python/util/image/image_object.py +++ b/src/main/python/util/image/image_object.py @@ -16,14 +16,18 @@ def get_image(self, type=IMAGE_TYPE.PIL): elif type == IMAGE_TYPE.CV: open_cv_image = np.array(self.image.convert('RGB')) # Convert RGB to BGR - return open_cv_image[:, :, ::-1].copy() + return open_cv_image[:, :, ::-1].copy() def cv_to_pil(self, image): # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) return Image.fromarray(image) - def base_64(self): + def base_64(self, resized=True): buffered = BytesIO() - self.image.save(buffered, format="JPEG") + im = self.image + if resized: + size = 1280, 720 + im.thumbnail(size, Image.ANTIALIAS) + im.save(buffered, format="JPEG") img_byte = buffered.getvalue() return base64.b64encode(img_byte).decode() \ No newline at end of file diff --git a/src/main/python/anki/word_audio.py b/src/main/python/util/word_audio.py similarity index 57% rename from src/main/python/anki/word_audio.py rename to src/main/python/util/word_audio.py index 7e631e5..4502001 100644 --- a/src/main/python/anki/word_audio.py +++ b/src/main/python/util/word_audio.py @@ -1,5 +1,6 @@ import requests import base64 +import hashlib def get_jpod_audio(url): try: @@ -16,13 +17,22 @@ def validate_jpod_audio_url(url): else: return False +def audioIsPlaceholder(data): + m = hashlib.md5() + m.update(data) + return m.hexdigest() == '7e2c2f954ef6051373ba916f000168dc' + def get_jpod_audio_url(kanji, kana): url = 'https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji={}&kana={}'.format(kanji, kana) return url if (validate_jpod_audio_url(url)) else '' -def get_jpod_audio_base64(url): - jpod_audio = get_jpod_audio(url) - if jpod_audio: - return 'data:audio/mp3;base64,' + str(base64.b64encode(jpod_audio.content)) - else: - return None \ No newline at end of file +def get_jpod_audio_base64(kanji, kana): + jpod_url = get_jpod_audio_url(kanji, kana) + if jpod_url: + print('jpod', jpod_url) + jpod_audio = get_jpod_audio(jpod_url) + if jpod_audio: + return 'data:audio/mp3;base64,' + str(base64.b64encode(jpod_audio.content)) + else: + return '' + return '' \ No newline at end of file diff --git a/src/main/resources/base/config.ini b/src/main/resources/base/config.ini new file mode 100644 index 0000000..c5647aa --- /dev/null +++ b/src/main/resources/base/config.ini @@ -0,0 +1,5 @@ +[IMAGECONFIG] +resize_screenshot = true +resize_screenshot_max_width = 1280 +resize_screenshot_max_height = 720 + diff --git a/src/main/resources/base/rikaisama/data.js b/src/main/resources/base/rikaisama/data.js index e7a08f7..e7daf9b 100644 --- a/src/main/resources/base/rikaisama/data.js +++ b/src/main/resources/base/rikaisama/data.js @@ -782,7 +782,6 @@ var rcxData = { b.push('
'); b.push(box); - b.push('
anki here
'); b.push('' + entry.kanji + '
'); if (!rcxConfig.hidedef) b.push('
' + entry.eigo + '
'); b.push('
' + yomi + '
'); @@ -865,7 +864,7 @@ var rcxData = { } if (e[2]) { - b.push('
+ anki
'); + // b.push('
+ anki
'); if (pK == e[1]) k = '\u3001 ' + e[2] + ''; else k += '' + e[1] + ' ' + e[2] + ''; pK = e[1]; diff --git a/src/main/resources/base/rikaisama/raikaichan.js b/src/main/resources/base/rikaisama/raikaichan.js index aa2db71..ee68293 100644 --- a/src/main/resources/base/rikaisama/raikaichan.js +++ b/src/main/resources/base/rikaisama/raikaichan.js @@ -588,7 +588,7 @@ var rcxMain = { } else { - console.error("showPopup(): elem or parentNode is not defined!"); + // console.error("showPopup(): elem or parentNode is not defined!"); } popup.style.left = (x + content.scrollX) + 'px'; @@ -727,21 +727,40 @@ var rcxMain = { }, copyToClip: function() { - console.error('copyToClip not impelemented'); - return; - var text; - - if ((text = this.savePrep(1, rcxConfig.saveformat)) != null) { - Components.classes['@mozilla.org/widget/clipboardhelper;1'] - .getService(Components.interfaces.nsIClipboardHelper) - .copyString(text); - this.showPopup('Copied to clipboard.'); - } else { - this.showPopup('Please select something to copy in Preferences.'); - return; - } + if(this.lastFound[0].data) + { + var entryData = this.lastFound[0].data[0][0].match(/^(.+?)\s+(?:\[(.*?)\])?\s*\/(.+)\//); + var expression = entryData[1]; + handler.copy_to_clipboard(expression); + this.showPopup(expression + ' copied to clipboard.'); + } + // var text; + // if ((text = this.savePrep(1, rcxConfig.saveformat)) != null) { + // navigator.clipboard.writeText(text); + // this.showPopup('Copied to clipboard.'); + // } else { + // this.showPopup('Please select something to copy in Preferences.'); + // return; + // } }, + scrollForwardDefinition: function() + { + if(this.lastFound[0].data) + { + var numberOfDefinitions = this.lastFound[0].data.length; + if (this.definitionPosition === undefined || this.lastFoundDefinition == undefined || this.lastFoundDefinition != this.lastFound[0].data) { + this.definitionPosition = 0; + this.lastFoundDefinition = this.lastFound[0].data; + } else if (this.definitionPosition == numberOfDefinitions-1) { + this.definitionPosition = 0; + } else { + this.definitionPosition += 1; + } + + console.error(this.definitionPosition); + } + }, /* Get the CSS style to use when drawing the provided frequency */ getFreqStyle: function(inFreqNum) @@ -1108,6 +1127,7 @@ var rcxMain = { 'definition': definition, 'sentence': this.sentence })); + this.showPopup(expression + ' added to Anki.'); } @@ -2979,6 +2999,17 @@ var rcxMain = { this.sendToAnki(); break; + case 67: // c - copy to clipboard + this.copyToClip(); + break; + + case 74: // j - scroll back definition + break; + + case 75: // k - scoll forward definition + this.scrollForwardDefinition(); + break; + case parseInt(rcxConfig.kbalternateview): // a - Alternate popup location this.allowOneTimeSuperSticky();