From 8308e70e963d3c602b5f322d1b6a88e9e986d8bc Mon Sep 17 00:00:00 2001 From: Mika Vilpas Date: Tue, 21 Mar 2023 19:42:48 +0200 Subject: [PATCH] add support for adding audio via the ankiconnect android app --- sakura/README.md | 111 +- sakura/package.json | 4 +- sakura/src/App.css | 3 - sakura/src/App.js | 2 +- sakura/src/utils/ClearableSearch.js | 1 - .../src/utils/yomichan/yomichanDatabase.tsx | 1 + sakura/src/views/dict/Definitions.js | 2 +- sakura/src/views/dict/Dictionaries.js | 1 - sakura/src/views/dict/SearchBox.js | 2 +- sakura/src/views/dict/index.js | 2 +- sakura/src/views/export/ExportView.tsx | 163 +- sakura/src/views/export/ResultItemSection.tsx | 29 + sakura/src/views/grammar/GrammarView.js | 2 +- sakura/src/views/help/AnkiConnectSettings.tsx | 76 +- sakura/src/views/help/FieldNameSelection.tsx | 41 + sakura/src/views/help/SettingsView.tsx | 10 +- .../help/dictionaries/ExistingDictionary.js | 2 +- .../dictionaries/ImportDictionaryWizard.js | 2 +- sakura/src/views/recursiveLookup/index.js | 2 +- .../versionIndicator/VersionIndicator.js | 2 - sakura/yarn.lock | 8483 +++++++++-------- 21 files changed, 4693 insertions(+), 4248 deletions(-) create mode 100644 sakura/src/views/export/ResultItemSection.tsx create mode 100644 sakura/src/views/help/FieldNameSelection.tsx diff --git a/sakura/README.md b/sakura/README.md index 7bebf5d..04823b7 100644 --- a/sakura/README.md +++ b/sakura/README.md @@ -1,79 +1,70 @@ -# how to build a new version - -Cd into this directory +# Common development instructions ```sh -(cd ../loaderapp && ./publish.sh) - -``` - -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `yarn start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `yarn test` +# install dependencies +yarn install -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +# start the development server +yarn start -### `yarn build` +# run unit tests +npx cypress run-ct -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +# run browser tests +npx cypress run -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +# build a new version for deployment +(cd ../loaderapp && ./publish.sh) +``` -To learn React, check out the [React documentation](https://reactjs.org/). +# How to develop against Ankiconnect Android -### Code Splitting +These instructions are for developing against the Android version of Ankiconnect +called Ankiconnect Android. This will work for the following scenarios: -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +1. You have an android device such as your mobile phone or tablet. +2. You are using an existing version of Ankiconnect Android, or you are + developing a version with Android Studio and you can run it on your device. -### Analyzing the Bundle Size +## Prerequisites for development -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +These instructions are for Chrome on a Mac. If you are using a different browser +or operating system, you may need to find the equivalent steps. -### Making a Progressive Web App +- Install Ankiconnect Android on your device +- Connect your device to your computer and enable USB debugging in Android. +- Connect your Android device to the same network as your computer. -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +## Connecting to hare on your Android device -### Advanced Configuration +1. Find out the IP address of your computer. For example, if you are on a + Mac, you can run `ifconfig | grep inet` and look for the IP address that + starts with 192.168.0. or 10.0.0. +1. In the Ankiconnect Android app, go to the settings and change the cors setting + to `*`. This will allow access from any host. If you want to limit this, you + can also set it to `http://:4000`. The + settings will be taken into use immediately. +1. Start hare on your computer with `yarn start`. This will make it available to + the network so you can connect to it from your device. +1. On your device, open http://:4000 -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) +## Connect your computer's chrome to your Android device's chrome -### Deployment +1. Connect your Android device to your computer with a USB cable. +2. Open Chrome on your computer and go to chrome://inspect/#devices . You will + be presented with a list of tabs on your device. Choose the tab that is running + hare. +3. Now you can use the developer tools on your computer to debug the code on your + device. Run the following code to test the connection: -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) +```js +fetch("http://localhost:8765") + .then((r) => r.text()) + .then(console.log); +``` -### `yarn build` fails to minify +It will respond with "Ankiconnect Android is running.". If you get a CORS error, +double check that you have set the cors setting in the app to `*` or +`http://:4000`. -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +NOTE: on your device, you need to have the chrome tab active for this to work. diff --git a/sakura/package.json b/sakura/package.json index 46ed017..e8b8467 100644 --- a/sakura/package.json +++ b/sakura/package.json @@ -37,11 +37,11 @@ "@cypress/webpack-dev-server": "^1.3.0", "@types/lodash": "^4.14.191", "@types/react-router-dom": "^5.3.3", - "cypress": "8.1.0", + "cypress": "8.5.0", "cypress-file-upload": "^5.0.8", "fake-indexeddb": "^3.1.3", "prettier": "^2.3.1", - "prettier-plugin-organize-imports": "^2.1.0", + "prettier-plugin-organize-imports": "^3.2.2", "react-json-view": "^1.21.3", "typescript": "^4.9.4", "worker-loader": "^3.0.8" diff --git a/sakura/src/App.css b/sakura/src/App.css index fa5ffcd..bc586f7 100644 --- a/sakura/src/App.css +++ b/sakura/src/App.css @@ -273,9 +273,6 @@ superscript { background-color: black; } -#delete-confirmation { -} - #delete-confirmation .title { background-color: #212529; } diff --git a/sakura/src/App.js b/sakura/src/App.js index 914fef8..57dad43 100644 --- a/sakura/src/App.js +++ b/sakura/src/App.js @@ -1,6 +1,6 @@ import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap/dist/css/bootstrap.min.css"; -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Container from "react-bootstrap/Container"; import { HashRouter as Router, diff --git a/sakura/src/utils/ClearableSearch.js b/sakura/src/utils/ClearableSearch.js index 03138c0..705e9ee 100644 --- a/sakura/src/utils/ClearableSearch.js +++ b/sakura/src/utils/ClearableSearch.js @@ -1,4 +1,3 @@ -import React from "react"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; diff --git a/sakura/src/utils/yomichan/yomichanDatabase.tsx b/sakura/src/utils/yomichan/yomichanDatabase.tsx index 3581873..b76da4c 100644 --- a/sakura/src/utils/yomichan/yomichanDatabase.tsx +++ b/sakura/src/utils/yomichan/yomichanDatabase.tsx @@ -33,6 +33,7 @@ export interface YomichanTerm { } export type AnkiFieldContentType = + | "(empty)" | "sentence" | "definition" | "englishTranslation" diff --git a/sakura/src/views/dict/Definitions.js b/sakura/src/views/dict/Definitions.js index a608dc9..4e0d5c1 100644 --- a/sakura/src/views/dict/Definitions.js +++ b/sakura/src/views/dict/Definitions.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import Accordion from "react-bootstrap/Accordion"; import Alert from "react-bootstrap/Alert"; import Button from "react-bootstrap/Button"; diff --git a/sakura/src/views/dict/Dictionaries.js b/sakura/src/views/dict/Dictionaries.js index 059393c..d29a288 100644 --- a/sakura/src/views/dict/Dictionaries.js +++ b/sakura/src/views/dict/Dictionaries.js @@ -1,4 +1,3 @@ -import React from "react"; import Alert from "react-bootstrap/Alert"; import ButtonGroup from "react-bootstrap/ButtonGroup"; import Spinner from "react-bootstrap/Spinner"; diff --git a/sakura/src/views/dict/SearchBox.js b/sakura/src/views/dict/SearchBox.js index f687294..cbaa8e8 100644 --- a/sakura/src/views/dict/SearchBox.js +++ b/sakura/src/views/dict/SearchBox.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; diff --git a/sakura/src/views/dict/index.js b/sakura/src/views/dict/index.js index 141f80f..08ecf02 100644 --- a/sakura/src/views/dict/index.js +++ b/sakura/src/views/dict/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { generatePath, useHistory, diff --git a/sakura/src/views/export/ExportView.tsx b/sakura/src/views/export/ExportView.tsx index 9171e91..0629497 100644 --- a/sakura/src/views/export/ExportView.tsx +++ b/sakura/src/views/export/ExportView.tsx @@ -1,13 +1,5 @@ import copy from "copy-to-clipboard"; -import React, { - ReactElement, - ReactNode, - useEffect, - useRef, - useState, -} from "react"; -import { DOMElement } from "react"; -import { RefCallback } from "react"; +import { ReactNode, RefCallback, useEffect, useRef, useState } from "react"; import Alert from "react-bootstrap/Alert"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; @@ -29,12 +21,14 @@ import { import * as wordParser from "../../utils/wordParser"; import YomichanDatabase, { AnkiConnectSettingData, + AnkiFieldContentType, YomichanDictionary, } from "../../utils/yomichan/yomichanDatabase"; import ExportViewDefinitionTokenProcessor from "../dict/tokenProcessors/exportViewDefinitionTokenProcessor"; import ToPlainTextTokenProcessor from "../dict/tokenProcessors/toPlainTextTokenProcessor"; import { prettyText } from "../dict/utils"; import Navbar from "../navbar/Navbar"; +import { ResultItemSection } from "./ResultItemSection"; type ErrorItemProps = { heading: string; error: any }; const ErrorItem = ({ heading, error }: ErrorItemProps) => { @@ -113,6 +107,7 @@ const SearchLinkWithIcon = ({ src={iconUrl} style={{ height: "16px", width: "16px" }} className="inline icon" + alt="search" > ); return ; @@ -122,7 +117,7 @@ type SelectSentenceForAnkiCardButtonProps = { onSelect: () => void; isSelected: boolean; }; -const SelectSentenceForAnkiCardButton = ({ +export const SelectSentenceForAnkiCardButton = ({ onSelect, isSelected, }: SelectSentenceForAnkiCardButtonProps) => { @@ -143,7 +138,10 @@ const SelectSentenceForAnkiCardButton = ({ }; type CopyQuoteButtonProps = { text: string; selectedWord?: string }; -const CopyQuoteButton = ({ text, selectedWord }: CopyQuoteButtonProps) => { +export const CopyQuoteButton = ({ + text, + selectedWord, +}: CopyQuoteButtonProps) => { const [wordWasCopied, setWordWasCopied] = useState(false); const copiableText = text @@ -207,19 +205,14 @@ const ExportView = ({ const [selectedWord, setSelectedWord] = useState(); const buttonsRef = useRef(); - const [selectedAnkiJapSentence, setSelectedAnkiJapSentence] = useState< - string - >(); - const [selectedAnkiEngSentence, setSelectedAnkiEngSentence] = useState< - string - >(); - const [ - selectedAnkiAudioSentenceUrl, - setSelectedAnkiAudioSentenceUrl, - ] = useState(); - const [ankiCardCreationError, setAnkiCardCreationError] = useState< - ReactNode - >(); + const [selectedAnkiJapSentence, setSelectedAnkiJapSentence] = + useState(); + const [selectedAnkiEngSentence, setSelectedAnkiEngSentence] = + useState(); + const [selectedAnkiAudioSentenceUrl, setSelectedAnkiAudioSentenceUrl] = + useState(); + const [ankiCardCreationError, setAnkiCardCreationError] = + useState(); useEffect(() => { pageView("export", `/${dict}`); @@ -243,13 +236,13 @@ const ExportView = ({ const sentence = q.dataset.quote || ""; ReactDOM.render( - <> +
setSelectedAnkiJapSentence(sentence)} isSelected={sentence === selectedAnkiJapSentence} /> - , +
, q ); }); @@ -412,63 +405,56 @@ const ExportView = ({ {audioSentences.map((sentenceRecord) => ( - - - {sentenceRecord.jap} - - - - - setSelectedAnkiJapSentence(sentenceRecord.jap) - } - isSelected={ - sentenceRecord.jap === selectedAnkiJapSentence - } + + - + {sentenceRecord.eng ? ( - <> - - {sentenceRecord.eng} - - - - - setSelectedAnkiEngSentence(sentenceRecord.eng) - } - isSelected={ - sentenceRecord.eng === selectedAnkiEngSentence - } - /> - + ) : null} - - - + + + - - - setSelectedAnkiAudioSentenceUrl( - sentenceRecord.audio_jap - ) - } - isSelected={ - sentenceRecord.audio_jap === - selectedAnkiAudioSentenceUrl - } - /> + + setSelectedAnkiAudioSentenceUrl( + sentenceRecord.audio_jap + ) + } + isSelected={ + sentenceRecord.audio_jap === + selectedAnkiAudioSentenceUrl + } + /> ))} @@ -507,7 +493,7 @@ const ExportView = ({ const baseUrl = ankiConnectSettings.address; let audioFields: string[] = []; - const options = { + const options: any = { deckName: ankiConnectSettings.selectedDeckName, modelName: ankiConnectSettings.selectedModelName, fields: Object.fromEntries( @@ -515,7 +501,8 @@ const ExportView = ({ ankiConnectSettings.fieldValueMapping ).map(([key, value]) => { let fieldContent = ""; - switch (value) { + const newValue = value as AnkiFieldContentType; + switch (newValue) { case "audio": { // will be added as a downloadable audio file instead audioFields.push(key); @@ -542,22 +529,34 @@ const ExportView = ({ fieldContent = selectedWord; break; } + case "(empty)": { + fieldContent = ""; + break; + } + default: { + // make sure that all cases are handled or it's a compile error + newValue satisfies never; + } } return [key, fieldContent]; }) ), - audio: selectedAnkiAudioSentenceUrl - ? { - url: selectedAnkiAudioSentenceUrl, - filename: - `hare_${selectedWord}_` + - new Date().toISOString(), - fields: audioFields, - } - : undefined, }; + if (selectedAnkiAudioSentenceUrl) { + // get the last "." separated part of the url + // which should be the file extension + const extension = + selectedAnkiAudioSentenceUrl.split(".").slice(-1)[0] || + "mp3"; + options.audio = { + url: selectedAnkiAudioSentenceUrl, + filename: `hare_${selectedWord}_${new Date().toISOString()}.${extension}`, + fields: audioFields, + }; + } + const response = await addNote(baseUrl, options); if (response.error) { setAnkiCardCreationError( diff --git a/sakura/src/views/export/ResultItemSection.tsx b/sakura/src/views/export/ResultItemSection.tsx new file mode 100644 index 0000000..21d4430 --- /dev/null +++ b/sakura/src/views/export/ResultItemSection.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { CopyQuoteButton, SelectSentenceForAnkiCardButton } from "./ExportView"; + +interface ResultItemSectionProps { + sentence: string; + selectedAnkiSentence?: string; + setSelectedAnkiSentence: React.Dispatch< + React.SetStateAction + >; +} +export const ResultItemSection = ({ + sentence, + selectedAnkiSentence, + setSelectedAnkiSentence, +}: ResultItemSectionProps) => { + return ( + <> + {sentence} +
+ + + setSelectedAnkiSentence(sentence)} + isSelected={sentence === selectedAnkiSentence} + /> +
+ + ); +}; diff --git a/sakura/src/views/grammar/GrammarView.js b/sakura/src/views/grammar/GrammarView.js index ec03929..57938f1 100644 --- a/sakura/src/views/grammar/GrammarView.js +++ b/sakura/src/views/grammar/GrammarView.js @@ -1,6 +1,6 @@ import axios from "axios"; import { QuickScore } from "quick-score"; -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Col from "react-bootstrap/Col"; import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; diff --git a/sakura/src/views/help/AnkiConnectSettings.tsx b/sakura/src/views/help/AnkiConnectSettings.tsx index 127e3a4..e546c23 100644 --- a/sakura/src/views/help/AnkiConnectSettings.tsx +++ b/sakura/src/views/help/AnkiConnectSettings.tsx @@ -1,54 +1,14 @@ /* eslint-disable import/no-webpack-loader-syntax */ -import React, { useEffect, useRef, useState } from "react"; -import Button from "react-bootstrap/Button"; +import { useEffect, useState } from "react"; import Col from "react-bootstrap/Col"; -import Container from "react-bootstrap/Container"; -import Form from "react-bootstrap/Form"; import Row from "react-bootstrap/Row"; -import { pageView } from "../../telemetry"; -import Navbar from "../navbar/Navbar"; -import ExistingDictionary from "./dictionaries/ExistingDictionary"; -import ImportDictionaryWizard from "./dictionaries/ImportDictionaryWizard"; import * as ankiconnectApi from "../../utils/ankiconnect/ankiconnectApi"; import { AnkiConnectSettingData, AnkiFieldContentType, } from "../../utils/yomichan/yomichanDatabase"; - -type FieldNameSelectionProps = { - value: string; - onChanged: (newValue: AnkiFieldContentType) => void; -}; -const FieldNameSelection = ({ value, onChanged }: FieldNameSelectionProps) => { - return ( - - ); -}; +import { FieldNameSelection } from "./FieldNameSelection"; type AnkiConnectSettingsProps = { ankiConnectSettings: AnkiConnectSettingData; @@ -98,10 +58,11 @@ const AnkiConnectSettingsComponent = ({ const showModelFieldNames = async () => { if (!ankiConnectSettings.selectedModelName) return; - const getModelFieldNamesResponse = await ankiconnectApi.getModelFieldNames( - baseUrl, - ankiConnectSettings.selectedModelName - ); + const getModelFieldNamesResponse = + await ankiconnectApi.getModelFieldNames( + baseUrl, + ankiConnectSettings.selectedModelName + ); setAnkiConnectError(getModelFieldNamesResponse.error); if (getModelFieldNamesResponse.error) { return; @@ -135,7 +96,7 @@ const AnkiConnectSettingsComponent = ({ - +