diff options
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 27 | ||||
-rw-r--r-- | src/ui/documentwidget.h | 1 | ||||
-rw-r--r-- | src/ui/translation.c | 360 | ||||
-rw-r--r-- | src/ui/translation.h | 45 | ||||
-rw-r--r-- | src/ui/util.c | 107 | ||||
-rw-r--r-- | src/ui/util.h | 3 |
7 files changed, 527 insertions, 18 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 35c7cb98..b4d7e659 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -158,6 +158,8 @@ set (SOURCES | |||
158 | src/ui/text.h | 158 | src/ui/text.h |
159 | src/ui/touch.c | 159 | src/ui/touch.c |
160 | src/ui/touch.h | 160 | src/ui/touch.h |
161 | src/ui/translation.c | ||
162 | src/ui/translation.h | ||
161 | src/ui/util.c | 163 | src/ui/util.c |
162 | src/ui/util.h | 164 | src/ui/util.h |
163 | src/ui/visbuf.c | 165 | src/ui/visbuf.c |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 79e8b727..a468e2df 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -43,6 +43,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
43 | #include "paint.h" | 43 | #include "paint.h" |
44 | #include "mediaui.h" | 44 | #include "mediaui.h" |
45 | #include "scrollwidget.h" | 45 | #include "scrollwidget.h" |
46 | #include "translation.h" | ||
46 | #include "util.h" | 47 | #include "util.h" |
47 | #include "visbuf.h" | 48 | #include "visbuf.h" |
48 | #include "visited.h" | 49 | #include "visited.h" |
@@ -218,6 +219,7 @@ struct Impl_DocumentWidget { | |||
218 | iPtrSet * invalidRuns; | 219 | iPtrSet * invalidRuns; |
219 | SDL_Texture * sideIconBuf; | 220 | SDL_Texture * sideIconBuf; |
220 | iTextBuf * timestampBuf; | 221 | iTextBuf * timestampBuf; |
222 | iTranslation * translation; | ||
221 | }; | 223 | }; |
222 | 224 | ||
223 | iDefineObjectConstruction(DocumentWidget) | 225 | iDefineObjectConstruction(DocumentWidget) |
@@ -273,6 +275,7 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
273 | d->playerMenu = NULL; | 275 | d->playerMenu = NULL; |
274 | d->sideIconBuf = NULL; | 276 | d->sideIconBuf = NULL; |
275 | d->timestampBuf = NULL; | 277 | d->timestampBuf = NULL; |
278 | d->translation = NULL; | ||
276 | addChildFlags_Widget(w, | 279 | addChildFlags_Widget(w, |
277 | iClob(new_IndicatorWidget()), | 280 | iClob(new_IndicatorWidget()), |
278 | resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); | 281 | resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); |
@@ -289,6 +292,7 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
289 | } | 292 | } |
290 | 293 | ||
291 | void deinit_DocumentWidget(iDocumentWidget *d) { | 294 | void deinit_DocumentWidget(iDocumentWidget *d) { |
295 | delete_Translation(d->translation); | ||
292 | if (d->sideIconBuf) { | 296 | if (d->sideIconBuf) { |
293 | SDL_DestroyTexture(d->sideIconBuf); | 297 | SDL_DestroyTexture(d->sideIconBuf); |
294 | } | 298 | } |
@@ -751,7 +755,7 @@ static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { | |||
751 | d->lastVisibleRun = NULL; | 755 | d->lastVisibleRun = NULL; |
752 | } | 756 | } |
753 | 757 | ||
754 | static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { | 758 | void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { |
755 | setUrl_GmDocument(d->doc, d->mod.url); | 759 | setUrl_GmDocument(d->doc, d->mod.url); |
756 | setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); | 760 | setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); |
757 | documentRunsInvalidated_DocumentWidget_(d); | 761 | documentRunsInvalidated_DocumentWidget_(d); |
@@ -825,7 +829,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode | |||
825 | } | 829 | } |
826 | setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); | 830 | setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); |
827 | setFormat_GmDocument(d->doc, gemini_GmDocumentFormat); | 831 | setFormat_GmDocument(d->doc, gemini_GmDocumentFormat); |
828 | setSource_DocumentWidget_(d, src); | 832 | setSource_DocumentWidget(d, src); |
829 | updateTheme_DocumentWidget_(d); | 833 | updateTheme_DocumentWidget_(d); |
830 | init_Anim(&d->scrollY, 0); | 834 | init_Anim(&d->scrollY, 0); |
831 | init_Anim(&d->sideOpacity, 0); | 835 | init_Anim(&d->sideOpacity, 0); |
@@ -947,7 +951,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
947 | } | 951 | } |
948 | } | 952 | } |
949 | if (setSource) { | 953 | if (setSource) { |
950 | setSource_DocumentWidget_(d, &str); | 954 | setSource_DocumentWidget(d, &str); |
951 | } | 955 | } |
952 | deinit_String(&str); | 956 | deinit_String(&str); |
953 | } | 957 | } |
@@ -1777,6 +1781,20 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1777 | cacheDocumentGlyphs_DocumentWidget_(d); | 1781 | cacheDocumentGlyphs_DocumentWidget_(d); |
1778 | return iFalse; | 1782 | return iFalse; |
1779 | } | 1783 | } |
1784 | else if (equalWidget_Command(cmd, w, "document.translate")) { | ||
1785 | if (!d->translation) { | ||
1786 | d->translation = new_Translation(d); | ||
1787 | } | ||
1788 | return iTrue; | ||
1789 | } | ||
1790 | else if (startsWith_CStr(cmd, "translation.") && d->translation) { | ||
1791 | const iBool wasHandled = handleCommand_Translation(d->translation, cmd); | ||
1792 | if (isFinished_Translation(d->translation)) { | ||
1793 | delete_Translation(d->translation); | ||
1794 | d->translation = NULL; | ||
1795 | } | ||
1796 | return wasHandled; | ||
1797 | } | ||
1780 | else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { | 1798 | else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { |
1781 | return handleMediaCommand_DocumentWidget_(d, cmd); | 1799 | return handleMediaCommand_DocumentWidget_(d, cmd); |
1782 | } | 1800 | } |
@@ -2524,9 +2542,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
2524 | { star_Icon " Subscribe to Page...", subscribeToPage_KeyModifier, "feeds.subscribe" }, | 2542 | { star_Icon " Subscribe to Page...", subscribeToPage_KeyModifier, "feeds.subscribe" }, |
2525 | { "---", 0, 0, NULL }, | 2543 | { "---", 0, 0, NULL }, |
2526 | { book_Icon " Import Links as Bookmarks...", 0, 0, "bookmark.links confirm:1" }, | 2544 | { book_Icon " Import Links as Bookmarks...", 0, 0, "bookmark.links confirm:1" }, |
2545 | { "Translate...", 0, 0, "document.translate" }, | ||
2527 | { "---", 0, 0, NULL }, | 2546 | { "---", 0, 0, NULL }, |
2528 | { "Copy Page URL", 0, 0, "document.copylink" } }, | 2547 | { "Copy Page URL", 0, 0, "document.copylink" } }, |
2529 | 11); | 2548 | 12); |
2530 | if (isEmpty_Range(&d->selectMark)) { | 2549 | if (isEmpty_Range(&d->selectMark)) { |
2531 | pushBackN_Array( | 2550 | pushBackN_Array( |
2532 | &items, | 2551 | &items, |
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h index f922e7ea..12603437 100644 --- a/src/ui/documentwidget.h +++ b/src/ui/documentwidget.h | |||
@@ -49,5 +49,6 @@ void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); | |||
49 | void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); | 49 | void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); |
50 | void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ | 50 | void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ |
51 | void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); | 51 | void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); |
52 | void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); | ||
52 | 53 | ||
53 | void updateSize_DocumentWidget (iDocumentWidget *); | 54 | void updateSize_DocumentWidget (iDocumentWidget *); |
diff --git a/src/ui/translation.c b/src/ui/translation.c new file mode 100644 index 00000000..cfb6eb79 --- /dev/null +++ b/src/ui/translation.c | |||
@@ -0,0 +1,360 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #include "translation.h" | ||
24 | |||
25 | #include "app.h" | ||
26 | #include "gmdocument.h" | ||
27 | #include "ui/command.h" | ||
28 | #include "ui/documentwidget.h" | ||
29 | #include "ui/labelwidget.h" | ||
30 | #include "ui/paint.h" | ||
31 | #include "ui/util.h" | ||
32 | |||
33 | #include <the_Foundation/regexp.h> | ||
34 | #include <SDL_timer.h> | ||
35 | #include <math.h> | ||
36 | |||
37 | /*----------------------------------------------------------------------------------------------*/ | ||
38 | |||
39 | iDeclareWidgetClass(TranslationProgressWidget) | ||
40 | iDeclareObjectConstruction(TranslationProgressWidget) | ||
41 | |||
42 | iDeclareType(Sprite) | ||
43 | |||
44 | struct Impl_Sprite { | ||
45 | iInt2 pos; | ||
46 | iInt2 size; | ||
47 | int color; | ||
48 | int xoff; | ||
49 | iString text; | ||
50 | }; | ||
51 | |||
52 | struct Impl_TranslationProgressWidget { | ||
53 | iWidget widget; | ||
54 | uint32_t startTime; | ||
55 | int font; | ||
56 | iArray sprites; | ||
57 | }; | ||
58 | |||
59 | void init_TranslationProgressWidget(iTranslationProgressWidget *d) { | ||
60 | iWidget *w = &d->widget; | ||
61 | init_Widget(w); | ||
62 | setId_Widget(w, "xlt.progress"); | ||
63 | init_Array(&d->sprites, sizeof(iSprite)); | ||
64 | d->startTime = SDL_GetTicks(); | ||
65 | /* Set up some letters to animate. */ | ||
66 | const char *chars = "ARGOS"; | ||
67 | const size_t n = strlen(chars); | ||
68 | resize_Array(&d->sprites, n); | ||
69 | d->font = uiContentBold_FontId; | ||
70 | const int width = lineHeight_Text(d->font); | ||
71 | const int gap = gap_Text / 2; | ||
72 | int x = (int) (n * width + (n - 1) * gap) / -2; | ||
73 | const int y = -lineHeight_Text(d->font) / 2; | ||
74 | for (size_t i = 0; i < n; i++) { | ||
75 | iSprite *spr = at_Array(&d->sprites, i); | ||
76 | spr->pos = init_I2(x, y); | ||
77 | spr->color = 0; | ||
78 | init_String(&spr->text); | ||
79 | appendChar_String(&spr->text, chars[i]); | ||
80 | spr->xoff = (width - advanceRange_Text(d->font, range_String(&spr->text)).x) / 2; | ||
81 | spr->size = init_I2(width, lineHeight_Text(d->font)); | ||
82 | x += width + gap; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | void deinit_TranslationProgressWidget(iTranslationProgressWidget *d) { | ||
87 | iForEach(Array, i, &d->sprites) { | ||
88 | iSprite *spr = i.value; | ||
89 | deinit_String(&spr->text); | ||
90 | } | ||
91 | deinit_Array(&d->sprites); | ||
92 | } | ||
93 | |||
94 | iDefineObjectConstruction(TranslationProgressWidget) | ||
95 | |||
96 | static void draw_TranslationProgressWidget_(const iTranslationProgressWidget *d) { | ||
97 | const iWidget *w = &d->widget; | ||
98 | const float t = (float) (SDL_GetTicks() - d->startTime) / 1000.0f; | ||
99 | const iRect bounds = bounds_Widget(w); | ||
100 | iPaint p; | ||
101 | init_Paint(&p); | ||
102 | const iInt2 mid = mid_Rect(bounds); | ||
103 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | ||
104 | iConstForEach(Array, i, &d->sprites) { | ||
105 | const int index = index_ArrayConstIterator(&i); | ||
106 | const float angle = (float) index; | ||
107 | const iSprite *spr = i.value; | ||
108 | const float opacity = iClamp(t - index * 0.5f, 0.0, 1.0f); | ||
109 | int bg = uiBackgroundSelected_ColorId; | ||
110 | int fg = uiTextSelected_ColorId; | ||
111 | iInt2 pos = add_I2(mid, spr->pos); | ||
112 | pos.y += sin(angle + t) * spr->size.y * iClamp(t * 0.25f - 0.3f, 0.0f, 1.0f); | ||
113 | if (bg >= 0) { | ||
114 | p.alpha = opacity * 255; | ||
115 | fillRect_Paint(&p, (iRect){ pos, spr->size }, bg); | ||
116 | } | ||
117 | if (fg >= 0) { | ||
118 | setOpacity_Text(opacity * 2); | ||
119 | drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text)); | ||
120 | } | ||
121 | } | ||
122 | setOpacity_Text(1.0f); | ||
123 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | ||
124 | } | ||
125 | |||
126 | static iBool processEvent_TranslationProgressWidget_(iTranslationProgressWidget *d, | ||
127 | const SDL_Event *ev) { | ||
128 | iUnused(d, ev); | ||
129 | return iFalse; | ||
130 | } | ||
131 | |||
132 | iBeginDefineSubclass(TranslationProgressWidget, Widget) | ||
133 | .draw = (iAny *) draw_TranslationProgressWidget_, | ||
134 | .processEvent = (iAny *) processEvent_TranslationProgressWidget_, | ||
135 | iEndDefineSubclass(TranslationProgressWidget) | ||
136 | |||
137 | /*----------------------------------------------------------------------------------------------*/ | ||
138 | |||
139 | iDefineTypeConstructionArgs(Translation, (iDocumentWidget *doc), doc) | ||
140 | |||
141 | static const char * translationServiceHost = "xlt.skyjake.fi"; | ||
142 | static const uint16_t translationServicePort = 443; | ||
143 | |||
144 | static const char *doubleArrowSymbol = "\u20e2"; /* prevent getting mangled */ | ||
145 | static const char *tripleBacktickSymbol = "\u20e3"; | ||
146 | static const char *h1Symbol = "\u20e4"; | ||
147 | static const char *h2Symbol = "\u20e5"; | ||
148 | static const char *h3Symbol = "\u20e6"; | ||
149 | |||
150 | static iString *quote_String_(const iString *d) { | ||
151 | iString *quot = new_String(); | ||
152 | iConstForEach(String, i, d) { | ||
153 | const iChar ch = i.value; | ||
154 | if (ch == '"') { | ||
155 | appendCStr_String(quot, "\\\""); | ||
156 | } | ||
157 | else if (ch == '\\') { | ||
158 | appendCStr_String(quot, "\\\\"); | ||
159 | } | ||
160 | else if (ch == '\n') { | ||
161 | appendCStr_String(quot, "\\n"); | ||
162 | } | ||
163 | else if (ch == '\r') { | ||
164 | appendCStr_String(quot, "\\r"); | ||
165 | } | ||
166 | else if (ch == '\t') { | ||
167 | appendCStr_String(quot, "\\t"); | ||
168 | } | ||
169 | else if (ch >= 0x100) { | ||
170 | appendFormat_String(quot, "\\u%04x", ch); | ||
171 | } | ||
172 | else { | ||
173 | appendChar_String(quot, ch); | ||
174 | } | ||
175 | } | ||
176 | return quot; | ||
177 | } | ||
178 | |||
179 | static iString *unquote_String_(const iString *d) { | ||
180 | iString *unquot = new_String(); | ||
181 | iConstForEach(String, i, d) { | ||
182 | const iChar ch = i.value; | ||
183 | if (ch == '\\') { | ||
184 | next_StringConstIterator(&i); | ||
185 | const iChar esc = i.value; | ||
186 | if (esc == '\\') { | ||
187 | appendChar_String(unquot, esc); | ||
188 | } | ||
189 | else if (esc == 'n') { | ||
190 | appendChar_String(unquot, '\n'); | ||
191 | } | ||
192 | else if (esc == 'r') { | ||
193 | appendChar_String(unquot, '\r'); | ||
194 | } | ||
195 | else if (esc == 't') { | ||
196 | appendChar_String(unquot, '\t'); | ||
197 | } | ||
198 | else if (esc == '"') { | ||
199 | appendChar_String(unquot, '"'); | ||
200 | } | ||
201 | else if (esc == 'u') { | ||
202 | char digits[5]; | ||
203 | iZap(digits); | ||
204 | iForIndices(j, digits) { | ||
205 | next_StringConstIterator(&i); | ||
206 | digits[j] = *i.pos; | ||
207 | } | ||
208 | iChar codepoint = strtoul(digits, NULL, 16); | ||
209 | if (codepoint) { | ||
210 | appendChar_String(unquot, codepoint); | ||
211 | } | ||
212 | } | ||
213 | else { | ||
214 | iAssert(0); | ||
215 | } | ||
216 | } | ||
217 | else { | ||
218 | appendChar_String(unquot, ch); | ||
219 | } | ||
220 | } | ||
221 | return unquot; | ||
222 | } | ||
223 | |||
224 | static void finished_Translation_(iTlsRequest *d, iTlsRequest *req) { | ||
225 | iUnused(req); | ||
226 | postCommandf_App("translation.finished ptr:%p", userData_Object(d)); | ||
227 | } | ||
228 | |||
229 | void init_Translation(iTranslation *d, iDocumentWidget *doc) { | ||
230 | d->dlg = makeTranslation_Widget(as_Widget(doc)); | ||
231 | d->startTime = 0; | ||
232 | d->doc = doc; /* owner */ | ||
233 | d->request = new_TlsRequest(); | ||
234 | d->timer = 0; | ||
235 | setUserData_Object(d->request, d->doc); | ||
236 | setHost_TlsRequest(d->request, | ||
237 | collectNewCStr_String(translationServiceHost), | ||
238 | translationServicePort); | ||
239 | iConnect(TlsRequest, d->request, finished, d->request, finished_Translation_); | ||
240 | } | ||
241 | |||
242 | void deinit_Translation(iTranslation *d) { | ||
243 | if (d->timer) { | ||
244 | SDL_RemoveTimer(d->timer); | ||
245 | } | ||
246 | cancel_TlsRequest(d->request); | ||
247 | iRelease(d->request); | ||
248 | destroy_Widget(d->dlg); | ||
249 | } | ||
250 | |||
251 | static uint32_t animate_Translation_(uint32_t interval, iAny *ptr) { | ||
252 | postCommandf_App("translation.update ptr:%p", ((iTranslation *) ptr)->doc); | ||
253 | return interval; | ||
254 | } | ||
255 | |||
256 | void submit_Translation(iTranslation *d) { | ||
257 | /* Check the selected languages from the dialog. */ | ||
258 | const char *idFrom = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.from"))); | ||
259 | const char *idTo = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.to"))); | ||
260 | iAssert(status_TlsRequest(d->request) != submitted_TlsRequestStatus); | ||
261 | iBlock *json = collect_Block(new_Block(0)); | ||
262 | iString *docSrc = collect_String(copy_String(source_GmDocument(document_DocumentWidget(d->doc)))); | ||
263 | replace_String(docSrc, "=>", doubleArrowSymbol); | ||
264 | replace_String(docSrc, "```", tripleBacktickSymbol); | ||
265 | replace_String(docSrc, "###", h3Symbol); | ||
266 | replace_String(docSrc, "##", h2Symbol); | ||
267 | replace_String(docSrc, "#", h1Symbol); | ||
268 | printf_Block(json, | ||
269 | "{\"q\":\"%s\",\"source\":\"%s\",\"target\":\"%s\"}", | ||
270 | cstrCollect_String(quote_String_(docSrc)), | ||
271 | idFrom, | ||
272 | idTo); | ||
273 | iBlock *msg = collect_Block(new_Block(0)); | ||
274 | printf_Block(msg, "POST /translate HTTP/1.1\r\n" | ||
275 | "Host: xlt.skyjake.fi\r\n" | ||
276 | "Connection: close\r\n" | ||
277 | "Content-Type: application/json\r\n" | ||
278 | "Content-Length: %zu\r\n\r\n", size_Block(json)); | ||
279 | append_Block(msg, json); | ||
280 | setContent_TlsRequest(d->request, msg); | ||
281 | submit_TlsRequest(d->request); | ||
282 | d->startTime = SDL_GetTicks(); | ||
283 | d->timer = SDL_AddTimer(1000 / 30, animate_Translation_, d); | ||
284 | } | ||
285 | |||
286 | static void processResult_Translation_(iTranslation *d) { | ||
287 | SDL_RemoveTimer(d->timer); | ||
288 | d->timer = 0; | ||
289 | if (status_TlsRequest(d->request) == error_TlsRequestStatus) { | ||
290 | return; | ||
291 | } | ||
292 | iBlock *resultData = collect_Block(readAll_TlsRequest(d->request)); | ||
293 | printf("result(%zu):\n%s\n", size_Block(resultData), cstr_Block(resultData)); | ||
294 | fflush(stdout); | ||
295 | iRegExp *pattern = iClob(new_RegExp(".*translatedText\":\"(.*)\"\\}", caseSensitive_RegExpOption)); | ||
296 | iRegExpMatch m; | ||
297 | init_RegExpMatch(&m); | ||
298 | if (matchRange_RegExp(pattern, range_Block(resultData), &m)) { | ||
299 | iString *translation = unquote_String_(collect_String(captured_RegExpMatch(&m, 1))); | ||
300 | replace_String(translation, tripleBacktickSymbol, "```"); | ||
301 | replace_String(translation, doubleArrowSymbol, "=>"); | ||
302 | replace_String(translation, h3Symbol, "###"); | ||
303 | replace_String(translation, h2Symbol, "##"); | ||
304 | replace_String(translation, h1Symbol, "#"); | ||
305 | setSource_DocumentWidget(d->doc, translation); | ||
306 | postCommand_App("sidebar.update"); | ||
307 | delete_String(translation); | ||
308 | } | ||
309 | else { | ||
310 | /* TODO: Report failure! */ | ||
311 | } | ||
312 | } | ||
313 | |||
314 | static iLabelWidget *acceptButton_Translation_(const iTranslation *d) { | ||
315 | return (iLabelWidget *) lastChild_Widget(findChild_Widget(d->dlg, "dialogbuttons")); | ||
316 | } | ||
317 | |||
318 | iBool handleCommand_Translation(iTranslation *d, const char *cmd) { | ||
319 | iWidget *w = as_Widget(d->doc); | ||
320 | if (equalWidget_Command(cmd, w, "translation.submit")) { | ||
321 | if (status_TlsRequest(d->request) == initialized_TlsRequestStatus) { | ||
322 | iWidget *langs = findChild_Widget(d->dlg, "xlt.langs"); | ||
323 | setFlags_Widget(langs, hidden_WidgetFlag, iTrue); | ||
324 | iLabelWidget *acceptButton = acceptButton_Translation_(d); | ||
325 | updateTextCStr_LabelWidget(acceptButton, "00:00"); | ||
326 | setFlags_Widget(as_Widget(acceptButton), disabled_WidgetFlag, iTrue); | ||
327 | iTranslationProgressWidget *prog = new_TranslationProgressWidget(); | ||
328 | setPos_Widget(as_Widget(prog), langs->rect.pos); | ||
329 | setSize_Widget(as_Widget(prog), langs->rect.size); | ||
330 | addChild_Widget(d->dlg, iClob(prog)); | ||
331 | submit_Translation(d); | ||
332 | } | ||
333 | return iTrue; | ||
334 | } | ||
335 | if (equalWidget_Command(cmd, w, "translation.update")) { | ||
336 | const uint32_t elapsed = SDL_GetTicks() - d->startTime; | ||
337 | const unsigned seconds = (elapsed / 1000) % 60; | ||
338 | const unsigned minutes = (elapsed / 60000); | ||
339 | updateText_LabelWidget(acceptButton_Translation_(d), | ||
340 | collectNewFormat_String("%02u:%02u", minutes, seconds)); | ||
341 | return iTrue; | ||
342 | } | ||
343 | if (equalWidget_Command(cmd, w, "translation.finished")) { | ||
344 | if (!isFinished_Translation(d)) { | ||
345 | processResult_Translation_(d); | ||
346 | destroy_Widget(d->dlg); | ||
347 | d->dlg = NULL; | ||
348 | } | ||
349 | return iTrue; | ||
350 | } | ||
351 | if (equalWidget_Command(cmd, d->dlg, "translation.cancel")) { | ||
352 | cancel_TlsRequest(d->request); | ||
353 | return iTrue; | ||
354 | } | ||
355 | return iFalse; | ||
356 | } | ||
357 | |||
358 | iBool isFinished_Translation(const iTranslation *d) { | ||
359 | return d->dlg == NULL; | ||
360 | } | ||
diff --git a/src/ui/translation.h b/src/ui/translation.h new file mode 100644 index 00000000..6966e468 --- /dev/null +++ b/src/ui/translation.h | |||
@@ -0,0 +1,45 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #pragma once | ||
24 | |||
25 | #include <the_Foundation/string.h> | ||
26 | #include <the_Foundation/tlsrequest.h> | ||
27 | |||
28 | iDeclareType(Translation) | ||
29 | iDeclareType(DocumentWidget) | ||
30 | iDeclareType(Widget) | ||
31 | |||
32 | struct Impl_Translation { | ||
33 | iTlsRequest * request; | ||
34 | iWidget * dlg; | ||
35 | uint32_t startTime; | ||
36 | iDocumentWidget *doc; | ||
37 | int timer; | ||
38 | }; | ||
39 | |||
40 | iDeclareTypeConstructionArgs(Translation, iDocumentWidget *) | ||
41 | |||
42 | void submit_Translation (iTranslation *); | ||
43 | iBool handleCommand_Translation (iTranslation *, const char *cmd); | ||
44 | |||
45 | iBool isFinished_Translation (const iTranslation *); | ||
diff --git a/src/ui/util.c b/src/ui/util.c index a2aaa893..61ecf948 100644 --- a/src/ui/util.c +++ b/src/ui/util.c | |||
@@ -1688,8 +1688,19 @@ static void appendFramelessTabPage_(iWidget *tabs, iWidget *page, const char *ti | |||
1688 | iTrue); | 1688 | iTrue); |
1689 | } | 1689 | } |
1690 | 1690 | ||
1691 | static iWidget *makeTwoColumnWidget_(iWidget **headings, iWidget **values) { | ||
1692 | iWidget *page = new_Widget(); | ||
1693 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | ||
1694 | *headings = addChildFlags_Widget( | ||
1695 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
1696 | *values = addChildFlags_Widget( | ||
1697 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
1698 | return page; | ||
1699 | } | ||
1700 | |||
1691 | static iWidget *appendTwoColumnPage_(iWidget *tabs, const char *title, int shortcut, iWidget **headings, | 1701 | static iWidget *appendTwoColumnPage_(iWidget *tabs, const char *title, int shortcut, iWidget **headings, |
1692 | iWidget **values) { | 1702 | iWidget **values) { |
1703 | /* TODO: Use `makeTwoColumnWidget_()`, see above. */ | ||
1693 | iWidget *page = new_Widget(); | 1704 | iWidget *page = new_Widget(); |
1694 | setFlags_Widget(page, arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | | 1705 | setFlags_Widget(page, arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | |
1695 | resizeHeightOfChildren_WidgetFlag | borderTop_WidgetFlag, iTrue); | 1706 | resizeHeightOfChildren_WidgetFlag | borderTop_WidgetFlag, iTrue); |
@@ -1935,13 +1946,8 @@ iWidget *makeBookmarkEditor_Widget(void) { | |||
1935 | iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", NULL)), | 1946 | iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", NULL)), |
1936 | frameless_WidgetFlag), | 1947 | frameless_WidgetFlag), |
1937 | "bmed.heading"); | 1948 | "bmed.heading"); |
1938 | iWidget *page = new_Widget(); | 1949 | iWidget *headings, *values; |
1939 | addChild_Widget(dlg, iClob(page)); | 1950 | addChild_Widget(dlg, iClob(makeTwoColumnWidget_(&headings, &values))); |
1940 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | ||
1941 | iWidget *headings = addChildFlags_Widget( | ||
1942 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
1943 | iWidget *values = addChildFlags_Widget( | ||
1944 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
1945 | iInputWidget *inputs[4]; | 1951 | iInputWidget *inputs[4]; |
1946 | addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); | 1952 | addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); |
1947 | setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); | 1953 | setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); |
@@ -2074,13 +2080,8 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { | |||
2074 | NULL)), | 2080 | NULL)), |
2075 | frameless_WidgetFlag), | 2081 | frameless_WidgetFlag), |
2076 | "feedcfg.heading"); | 2082 | "feedcfg.heading"); |
2077 | iWidget *page = new_Widget(); | 2083 | iWidget *headings, *values; |
2078 | addChild_Widget(dlg, iClob(page)); | 2084 | addChild_Widget(dlg, iClob(makeTwoColumnWidget_(&headings, &values))); |
2079 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | ||
2080 | iWidget *headings = addChildFlags_Widget( | ||
2081 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
2082 | iWidget *values = addChildFlags_Widget( | ||
2083 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
2084 | addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); | 2085 | addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); |
2085 | iInputWidget *input = new_InputWidget(0); | 2086 | iInputWidget *input = new_InputWidget(0); |
2086 | setId_Widget(addChild_Widget(values, iClob(input)), "feedcfg.title"); | 2087 | setId_Widget(addChild_Widget(values, iClob(input)), "feedcfg.title"); |
@@ -2184,3 +2185,81 @@ iWidget *makeIdentityCreation_Widget(void) { | |||
2184 | finalizeSheet_Widget(dlg); | 2185 | finalizeSheet_Widget(dlg); |
2185 | return dlg; | 2186 | return dlg; |
2186 | } | 2187 | } |
2188 | |||
2189 | static const iMenuItem languages[] = { | ||
2190 | { "Arabic", 0, 0, "xlt.lang id:ar" }, | ||
2191 | { "Chinese", 0, 0, "xlt.lang id:zh" }, | ||
2192 | { "English", 0, 0, "xlt.lang id:en" }, | ||
2193 | { "French", 0, 0, "xlt.lang id:fr" }, | ||
2194 | { "German", 0, 0, "xlt.lang id:de" }, | ||
2195 | { "Hindi", 0, 0, "xlt.lang id:hi" }, | ||
2196 | { "Italian", 0, 0, "xlt.lang id:it" }, | ||
2197 | { "Japanese", 0, 0, "xlt.lang id:ja" }, | ||
2198 | { "Portuguese", 0, 0, "xlt.lang id:pt" }, | ||
2199 | { "Russian", 0, 0, "xlt.lang id:ru" }, | ||
2200 | { "Spanish", 0, 0, "xlt.lang id:es" }, | ||
2201 | }; | ||
2202 | |||
2203 | static iBool translationHandler_(iWidget *dlg, const char *cmd) { | ||
2204 | if (equal_Command(cmd, "xlt.lang")) { | ||
2205 | iLabelWidget *menuItem = pointer_Command(cmd); | ||
2206 | iWidget *button = parent_Widget(parent_Widget(menuItem)); | ||
2207 | iAssert(isInstance_Object(button, &Class_LabelWidget)); | ||
2208 | updateText_LabelWidget((iLabelWidget *) button, text_LabelWidget(menuItem)); | ||
2209 | return iTrue; | ||
2210 | } | ||
2211 | return iFalse; | ||
2212 | } | ||
2213 | |||
2214 | const char *languageId_String(const iString *menuItemLabel) { | ||
2215 | iForIndices(i, languages) { | ||
2216 | if (!cmp_String(menuItemLabel, languages[i].label)) { | ||
2217 | return cstr_Rangecc(range_Command(languages[i].command, "id")); | ||
2218 | } | ||
2219 | } | ||
2220 | return ""; | ||
2221 | } | ||
2222 | |||
2223 | iWidget *makeTranslation_Widget(iWidget *parent) { | ||
2224 | iWidget *dlg = makeSheet_Widget("xlt"); | ||
2225 | setCommandHandler_Widget(dlg, translationHandler_); | ||
2226 | addChildFlags_Widget(dlg, | ||
2227 | iClob(new_LabelWidget(uiHeading_ColorEscape "TRANSLATE PAGE", NULL)), | ||
2228 | frameless_WidgetFlag); | ||
2229 | addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId)))); | ||
2230 | iWidget *headings, *values; | ||
2231 | iWidget *page; | ||
2232 | addChild_Widget(dlg, iClob(page = makeTwoColumnWidget_(&headings, &values))); | ||
2233 | setId_Widget(page, "xlt.langs"); | ||
2234 | addChild_Widget(headings, iClob(makeHeading_Widget("From:"))); | ||
2235 | iLabelWidget *fromLang, *toLang; | ||
2236 | setId_Widget(addChildFlags_Widget(values, | ||
2237 | iClob(fromLang = makeMenuButton_LabelWidget( | ||
2238 | "Portuguese", languages, iElemCount(languages))), | ||
2239 | alignLeft_WidgetFlag), | ||
2240 | "xlt.from"); | ||
2241 | updateTextCStr_LabelWidget(fromLang, "French"); /* TODO: Check source media type; remember last use. */ | ||
2242 | setBackgroundColor_Widget(findChild_Widget(as_Widget(fromLang), "menu"), | ||
2243 | uiBackgroundMenu_ColorId); | ||
2244 | addChild_Widget(headings, iClob(makeHeading_Widget("To:"))); | ||
2245 | setId_Widget(addChildFlags_Widget(values, | ||
2246 | iClob(toLang = makeMenuButton_LabelWidget( | ||
2247 | "Portuguese", languages, iElemCount(languages))), | ||
2248 | alignLeft_WidgetFlag), | ||
2249 | "xlt.to"); | ||
2250 | setBackgroundColor_Widget(findChild_Widget(as_Widget(toLang), "menu"), | ||
2251 | uiBackgroundMenu_ColorId); | ||
2252 | updateTextCStr_LabelWidget(toLang, "English"); /* TODO: User preference. */ | ||
2253 | addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId)))); | ||
2254 | addChild_Widget( | ||
2255 | dlg, | ||
2256 | iClob(makeDialogButtons_Widget( | ||
2257 | (iMenuItem[]){ | ||
2258 | { "Cancel", SDLK_ESCAPE, 0, "translation.cancel" }, | ||
2259 | { uiTextAction_ColorEscape "Translate", SDLK_RETURN, 0, "translation.submit" } }, | ||
2260 | 2))); | ||
2261 | addChild_Widget(parent, iClob(dlg)); | ||
2262 | arrange_Widget(dlg); | ||
2263 | finalizeSheet_Widget(dlg); | ||
2264 | return dlg; | ||
2265 | } | ||
diff --git a/src/ui/util.h b/src/ui/util.h index e2461b9d..251683f5 100644 --- a/src/ui/util.h +++ b/src/ui/util.h | |||
@@ -223,3 +223,6 @@ iWidget * makeBookmarkEditor_Widget (void); | |||
223 | iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon); | 223 | iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon); |
224 | iWidget * makeIdentityCreation_Widget (void); | 224 | iWidget * makeIdentityCreation_Widget (void); |
225 | iWidget * makeFeedSettings_Widget (uint32_t bookmarkId); | 225 | iWidget * makeFeedSettings_Widget (uint32_t bookmarkId); |
226 | iWidget * makeTranslation_Widget (iWidget *parent); | ||
227 | |||
228 | const char * languageId_String (const iString *menuItemLabel); | ||