add get custom covers
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibraryServerHtml.java
CommitLineData
33d40b9f
NR
1package be.nikiroo.fanfix.library;
2
3import java.io.File;
4import java.io.FileInputStream;
5import java.io.IOException;
6import java.io.InputStream;
7import java.security.KeyStore;
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11
12import javax.net.ssl.KeyManagerFactory;
13import javax.net.ssl.SSLServerSocketFactory;
14
15import be.nikiroo.fanfix.Instance;
16import be.nikiroo.fanfix.bundles.Config;
17import be.nikiroo.fanfix.bundles.UiConfig;
18import be.nikiroo.fanfix.data.Chapter;
19import be.nikiroo.fanfix.data.MetaData;
20import be.nikiroo.fanfix.data.Paragraph;
21import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
22import be.nikiroo.fanfix.data.Story;
23import be.nikiroo.fanfix.library.WebLibraryServer.WLoginResult;
24import be.nikiroo.fanfix.library.web.WebLibraryServerIndex;
25import be.nikiroo.fanfix.reader.TextOutput;
26import be.nikiroo.utils.IOUtils;
27import be.nikiroo.utils.NanoHTTPD;
28import be.nikiroo.utils.NanoHTTPD.IHTTPSession;
29import be.nikiroo.utils.NanoHTTPD.Response;
30import be.nikiroo.utils.NanoHTTPD.Response.Status;
31import be.nikiroo.utils.TraceHandler;
32import be.nikiroo.utils.Version;
33
34abstract class WebLibraryServerHtml implements Runnable {
35 private NanoHTTPD server;
36 protected TraceHandler tracer = new TraceHandler();
37
38 abstract protected WLoginResult login(String who, String cookie);
39
40 abstract protected WLoginResult login(String who, String key,
41 String subkey);
42
43 abstract protected WLoginResult login(boolean badLogin, boolean badCookie);
44
45 abstract protected Response getList(String uri, WLoginResult login)
46 throws IOException;
47
48 abstract protected Response getStoryPart(String uri, WLoginResult login);
49
6673ec59
NR
50 abstract protected Response getCover(String uri, WLoginResult login)
51 throws IOException;
52
33d40b9f
NR
53 abstract protected List<MetaData> metas(WLoginResult login)
54 throws IOException;
55
56 abstract protected Story story(String luid, WLoginResult login)
57 throws IOException;
58
59 public WebLibraryServerHtml(boolean secure) throws IOException {
60 Integer port = Instance.getInstance().getConfig()
61 .getInteger(Config.SERVER_PORT);
62 if (port == null) {
63 throw new IOException(
64 "Cannot start web server: port not specified");
65 }
66
67 SSLServerSocketFactory ssf = null;
68 if (secure) {
69 String keystorePath = Instance.getInstance().getConfig()
70 .getString(Config.SERVER_SSL_KEYSTORE, "");
71 String keystorePass = Instance.getInstance().getConfig()
72 .getString(Config.SERVER_SSL_KEYSTORE_PASS);
73
74 if (secure && keystorePath.isEmpty()) {
75 throw new IOException(
76 "Cannot start a secure web server: no keystore.jks file povided");
77 }
78
79 if (!keystorePath.isEmpty()) {
80 File keystoreFile = new File(keystorePath);
81 try {
82 KeyStore keystore = KeyStore
83 .getInstance(KeyStore.getDefaultType());
84 InputStream keystoreStream = new FileInputStream(
85 keystoreFile);
86 try {
87 keystore.load(keystoreStream,
88 keystorePass.toCharArray());
89 KeyManagerFactory keyManagerFactory = KeyManagerFactory
90 .getInstance(KeyManagerFactory
91 .getDefaultAlgorithm());
92 keyManagerFactory.init(keystore,
93 keystorePass.toCharArray());
94 ssf = NanoHTTPD.makeSSLSocketFactory(keystore,
95 keyManagerFactory);
96 } finally {
97 keystoreStream.close();
98 }
99 } catch (Exception e) {
100 throw new IOException(e.getMessage());
101 }
102 }
103 }
104
105 server = new NanoHTTPD(port) {
106 @Override
107 public Response serve(final IHTTPSession session) {
108 super.serve(session);
109
110 String query = session.getQueryParameterString(); // a=a%20b&dd=2
111 Method method = session.getMethod(); // GET, POST..
112 String uri = session.getUri(); // /home.html
113
114 // need them in real time (not just those sent by the UA)
115 Map<String, String> cookies = new HashMap<String, String>();
116 for (String cookie : session.getCookies()) {
117 cookies.put(cookie, session.getCookies().read(cookie));
118 }
119
120 WLoginResult login = null;
121 Map<String, String> params = session.getParms();
122 String who = session.getRemoteHostName()
123 + session.getRemoteIpAddress();
124 if (params.get("login") != null) {
125 login = login(who, params.get("password"),
126 params.get("login"));
127 } else {
128 String cookie = cookies.get("cookie");
129 login = login(who, cookie);
130 }
131
132 if (login.isSuccess()) {
133 // refresh cookie
134 session.getCookies().set(new Cookie("cookie",
135 login.getCookie(), "30; path=/"));
136
137 // set options
138 String optionName = params.get("optionName");
139 if (optionName != null && !optionName.isEmpty()) {
140 String optionNo = params.get("optionNo");
141 String optionValue = params.get("optionValue");
142 if (optionNo != null || optionValue == null
143 || optionValue.isEmpty()) {
144 session.getCookies().delete(optionName);
145 cookies.remove(optionName);
146 } else {
147 session.getCookies().set(new Cookie(optionName,
148 optionValue, "; path=/"));
149 cookies.put(optionName, optionValue);
150 }
151 }
152 }
153
154 Response rep = null;
155 if (!login.isSuccess() && WebLibraryUrls.isSupportedUrl(uri)) {
156 rep = loginPage(login, uri);
157 }
158
159 if (rep == null) {
160 try {
161 if (WebLibraryUrls.isSupportedUrl(uri)) {
162 if (WebLibraryUrls.INDEX_URL.equals(uri)) {
163 rep = root(session, cookies, login);
164 } else if (WebLibraryUrls.VERSION_URL.equals(uri)) {
165 rep = newFixedLengthResponse(Status.OK,
166 MIME_PLAINTEXT,
167 Version.getCurrentVersion().toString());
6673ec59
NR
168 } else if (WebLibraryUrls.isCoverUrl(uri)) {
169 rep = getCover(uri, login);
33d40b9f
NR
170 } else if (WebLibraryUrls.isListUrl(uri)) {
171 rep = getList(uri, login);
172 } else if (WebLibraryUrls.isStoryUrl(uri)) {
173 rep = getStoryPart(uri, login);
174 } else if (WebLibraryUrls.isViewUrl(uri)) {
175 rep = getViewer(cookies, uri, login);
176 } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
177 session.getCookies().delete("cookie");
178 cookies.remove("cookie");
179 rep = loginPage(login(false, false), uri);
180 } else {
181 getTraceHandler().error(
182 "Supported URL was not processed: "
183 + uri);
184 rep = newFixedLengthResponse(
185 Status.INTERNAL_ERROR,
186 NanoHTTPD.MIME_PLAINTEXT,
187 "An error happened");
188 }
189 } else {
190 if (uri.startsWith("/"))
191 uri = uri.substring(1);
192 InputStream in = IOUtils.openResource(
193 WebLibraryServerIndex.class, uri);
194 if (in != null) {
195 String mimeType = MIME_PLAINTEXT;
196 if (uri.endsWith(".css")) {
197 mimeType = "text/css";
198 } else if (uri.endsWith(".html")) {
199 mimeType = "text/html";
200 } else if (uri.endsWith(".js")) {
201 mimeType = "text/javascript";
202 }
203 rep = newChunkedResponse(Status.OK, mimeType,
204 in);
205 }
206
207 if (rep == null) {
208 getTraceHandler().trace("404: " + uri);
209 rep = newFixedLengthResponse(Status.NOT_FOUND,
210 NanoHTTPD.MIME_PLAINTEXT, "Not Found");
211 }
212 }
213 } catch (Exception e) {
214 Instance.getInstance().getTraceHandler().error(
215 new IOException("Cannot process web request",
216 e));
217 rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
218 NanoHTTPD.MIME_PLAINTEXT, "An error occured");
219 }
220 }
221
222 return rep;
223 }
224 };
225
6673ec59
NR
226 if (ssf != null)
227
228 {
33d40b9f
NR
229 getTraceHandler().trace("Install SSL on the web server...");
230 server.makeSecure(ssf, null);
231 getTraceHandler().trace("Done.");
232 }
233 }
234
235 @Override
236 public void run() {
237 try {
238 server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
239 } catch (IOException e) {
240 tracer.error(new IOException("Cannot start the web server", e));
241 }
242 }
243
244 /**
245 * The traces handler for this {@link WebLibraryServerHtml}.
246 *
247 * @return the traces handler
248 */
249 public TraceHandler getTraceHandler() {
250 return tracer;
251 }
252
253 /**
254 * The traces handler for this {@link WebLibraryServerHtml}.
255 *
256 * @param tracer
257 * the new traces handler
258 */
259 public void setTraceHandler(TraceHandler tracer) {
260 if (tracer == null) {
261 tracer = new TraceHandler(false, false, false);
262 }
263
264 this.tracer = tracer;
265 }
266
267 private Response loginPage(WLoginResult login, String uri) {
268 StringBuilder builder = new StringBuilder();
269
270 appendPreHtml(builder, true);
271
272 if (login.isBadLogin()) {
273 builder.append("<div class='error'>Bad login or password</div>");
274 } else if (login.isBadCookie()) {
275 builder.append("<div class='error'>Your session timed out</div>");
276 }
277
278 if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
279 uri = WebLibraryUrls.INDEX_URL;
280 }
281
282 builder.append(
283 "<form method='POST' action='" + uri + "' class='login'>\n");
284 builder.append(
285 "<p>You must be logged into the system to see the stories.</p>");
286 builder.append("\t<input type='text' name='login' />\n");
287 builder.append("\t<input type='password' name='password' />\n");
288 builder.append("\t<input type='submit' value='Login' />\n");
289 builder.append("</form>\n");
290
291 appendPostHtml(builder);
292
293 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
294 NanoHTTPD.MIME_HTML, builder.toString());
295 }
296
297 private Response root(IHTTPSession session, Map<String, String> cookies,
298 WLoginResult login) throws IOException {
299 BasicLibrary lib = Instance.getInstance().getLibrary();
300 MetaResultList result = new MetaResultList(metas(login));
301 StringBuilder builder = new StringBuilder();
302
303 appendPreHtml(builder, true);
304
305 Map<String, String> params = session.getParms();
306
307 String filter = cookies.get("filter");
308 if (params.get("optionNo") != null)
309 filter = null;
310 if (filter == null) {
311 filter = "";
312 }
313
314 String browser = params.get("browser") == null ? ""
315 : params.get("browser");
316 String browser2 = params.get("browser2") == null ? ""
317 : params.get("browser2");
318 String browser3 = params.get("browser3") == null ? ""
319 : params.get("browser3");
320
321 String filterSource = null;
322 String filterAuthor = null;
323 String filterTag = null;
324
325 // TODO: javascript in realtime, using visible=false + hide [submit]
326
327 builder.append("<form class='browser'>\n");
328 builder.append("<div class='breadcrumbs'>\n");
329
330 builder.append("\t<select name='browser'>");
331 appendOption(builder, 2, "", "", browser);
332 appendOption(builder, 2, "Sources", "sources", browser);
333 appendOption(builder, 2, "Authors", "authors", browser);
334 appendOption(builder, 2, "Tags", "tags", browser);
335 builder.append("\t</select>\n");
336
337 if (!browser.isEmpty()) {
338 builder.append("\t<select name='browser2'>");
339 if (browser.equals("sources")) {
340 filterSource = browser2.isEmpty() ? filterSource : browser2;
341 // TODO: if 1 group -> no group
342 appendOption(builder, 2, "", "", browser2);
343 Map<String, List<String>> sources = result.getSourcesGrouped();
344 for (String source : sources.keySet()) {
345 appendOption(builder, 2, source, source, browser2);
346 }
347 } else if (browser.equals("authors")) {
348 filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
349 // TODO: if 1 group -> no group
350 appendOption(builder, 2, "", "", browser2);
351 Map<String, List<String>> authors = result.getAuthorsGrouped();
352 for (String author : authors.keySet()) {
353 appendOption(builder, 2, author, author, browser2);
354 }
355 } else if (browser.equals("tags")) {
356 filterTag = browser2.isEmpty() ? filterTag : browser2;
357 appendOption(builder, 2, "", "", browser2);
358 for (String tag : result.getTags()) {
359 appendOption(builder, 2, tag, tag, browser2);
360 }
361 }
362 builder.append("\t</select>\n");
363 }
364
365 if (!browser2.isEmpty()) {
366 if (browser.equals("sources")) {
367 filterSource = browser3.isEmpty() ? filterSource : browser3;
368 Map<String, List<String>> sourcesGrouped = result
369 .getSourcesGrouped();
370 List<String> sources = sourcesGrouped.get(browser2);
371 if (sources != null && !sources.isEmpty()) {
372 // TODO: single empty value
373 builder.append("\t<select name='browser3'>");
374 appendOption(builder, 2, "", "", browser3);
375 for (String source : sources) {
376 appendOption(builder, 2, source, source, browser3);
377 }
378 builder.append("\t</select>\n");
379 }
380 } else if (browser.equals("authors")) {
381 filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
382 Map<String, List<String>> authorsGrouped = result
383 .getAuthorsGrouped();
384 List<String> authors = authorsGrouped.get(browser2);
385 if (authors != null && !authors.isEmpty()) {
386 // TODO: single empty value
387 builder.append("\t<select name='browser3'>");
388 appendOption(builder, 2, "", "", browser3);
389 for (String author : authors) {
390 appendOption(builder, 2, author, author, browser3);
391 }
392 builder.append("\t</select>\n");
393 }
394 }
395 }
396
397 builder.append("\t<input type='submit' value='Select'/>\n");
398 builder.append("</div>\n");
399
400 // TODO: javascript in realtime, using visible=false + hide [submit]
401 builder.append("<div class='filter'>\n");
402 builder.append("\t<span class='label'>Filter: </span>\n");
403 builder.append(
404 "\t<input name='optionName' type='hidden' value='filter' />\n");
405 builder.append("\t<input name='optionValue' type='text' value='"
406 + filter + "' place-holder='...' />\n");
407 builder.append("\t<input name='optionNo' type='submit' value='x' />");
408 builder.append(
409 "\t<input name='submit' type='submit' value='Filter' />\n");
410 builder.append("</div>\n");
411 builder.append("</form>\n");
412
413 builder.append("\t<div class='books'>");
414 for (MetaData meta : result.getMetas()) {
415 if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
416 .contains(filter.toLowerCase())) {
417 continue;
418 }
419
420 // TODO Sub sources
421 if (filterSource != null
422 && !filterSource.equals(meta.getSource())) {
423 continue;
424 }
425
426 // TODO: sub authors
427 if (filterAuthor != null
428 && !filterAuthor.equals(meta.getAuthor())) {
429 continue;
430 }
431
432 if (filterTag != null && !meta.getTags().contains(filterTag)) {
433 continue;
434 }
435
436 builder.append("<div class='book_line'>");
437 builder.append("<a href='");
438 builder.append(
439 WebLibraryUrls.getViewUrl(meta.getLuid(), null, null));
440 builder.append("'");
441 builder.append(" class='link'>");
442
443 if (lib.isCached(meta.getLuid())) {
444 // â—‰ = &#9673;
445 builder.append(
446 "<span class='cache_icon cached'>&#9673;</span>");
447 } else {
448 // â—‹ = &#9675;
449 builder.append(
450 "<span class='cache_icon uncached'>&#9675;</span>");
451 }
452 builder.append("<span class='luid'>");
453 builder.append(meta.getLuid());
454 builder.append("</span>");
455 builder.append("<span class='title'>");
456 builder.append(meta.getTitle());
457 builder.append("</span>");
458 builder.append("<span class='author'>");
459 if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
460 builder.append("(").append(meta.getAuthor()).append(")");
461 }
462 builder.append("</span>");
463 builder.append("</a></div>\n");
464 }
465 builder.append("</div>");
466
467 appendPostHtml(builder);
468 return NanoHTTPD.newFixedLengthResponse(builder.toString());
469 }
470
471 private Response getViewer(Map<String, String> cookies, String uri,
472 WLoginResult login) {
473 String[] cover = uri.split("/");
474 int off = 2;
475
476 if (cover.length < off + 2) {
477 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
478 NanoHTTPD.MIME_PLAINTEXT, null);
479 }
480
481 String type = cover[off + 0];
482 String luid = cover[off + 1];
483 String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
484 String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
485
486 // 1-based (0 = desc)
487 int chapter = 0;
488 if (chapterStr != null) {
489 try {
490 chapter = Integer.parseInt(chapterStr);
491 if (chapter < 0) {
492 throw new NumberFormatException();
493 }
494 } catch (NumberFormatException e) {
495 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
496 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
497 }
498 }
499
500 // 1-based
501 int paragraph = 0;
502 if (paragraphStr != null) {
503 try {
504 paragraph = Integer.parseInt(paragraphStr);
505 if (paragraph <= 0) {
506 throw new NumberFormatException();
507 }
508 } catch (NumberFormatException e) {
509 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
510 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
511 }
512 }
513
514 try {
515 Story story = story(luid, login);
516 if (story == null) {
517 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
518 NanoHTTPD.MIME_PLAINTEXT, "Story not found");
519 }
520
521 StringBuilder builder = new StringBuilder();
522 appendPreHtml(builder, false);
523
524 // For images documents, always go to the images if not chap 0 desc
525 if (story.getMeta().isImageDocument()) {
526 if (chapter > 0 && paragraph <= 0)
527 paragraph = 1;
528 }
529
530 Chapter chap = null;
531 if (chapter <= 0) {
532 chap = story.getMeta().getResume();
533 } else {
534 try {
535 chap = story.getChapters().get(chapter - 1);
536 } catch (IndexOutOfBoundsException e) {
537 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
538 NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
539 }
540 }
541
542 String first, previous, next, last;
543
544 StringBuilder content = new StringBuilder();
545
546 String disabledLeft = "";
547 String disabledRight = "";
548 String disabledZoomReal = "";
549 String disabledZoomWidth = "";
550 String disabledZoomHeight = "";
551
552 if (paragraph <= 0) {
553 first = WebLibraryUrls.getViewUrl(luid, 0, null);
554 previous = WebLibraryUrls.getViewUrl(luid,
555 (Math.max(chapter - 1, 0)), null);
556 next = WebLibraryUrls.getViewUrl(luid,
557 (Math.min(chapter + 1, story.getChapters().size())),
558 null);
559 last = WebLibraryUrls.getViewUrl(luid,
560 story.getChapters().size(), null);
561
562 StringBuilder desc = new StringBuilder();
563
564 if (chapter <= 0) {
565 desc.append("<h1 class='title'>");
566 desc.append(story.getMeta().getTitle());
567 desc.append("</h1>\n");
568 desc.append("<div class='desc'>\n");
569 desc.append("\t<a href='" + next + "' class='cover'>\n");
570 desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
571 desc.append("\t</a>\n");
572 desc.append("\t<table class='details'>\n");
573 Map<String, String> details = BasicLibrary
574 .getMetaDesc(story.getMeta());
575 for (String key : details.keySet()) {
576 appendTableRow(desc, 2, key, details.get(key));
577 }
578 desc.append("\t</table>\n");
579 desc.append("</div>\n");
580 desc.append("<h1 class='title'>Description</h1>\n");
581 }
582
583 content.append("<div class='viewer text'>\n");
584 content.append(desc);
585 String description = new TextOutput(false).convert(chap,
586 chapter > 0);
587 content.append(chap.getParagraphs().size() <= 0
588 ? "No content provided."
589 : description);
590 content.append("</div>\n");
591
592 if (chapter <= 0)
593 disabledLeft = " disabled='disbaled'";
594 if (chapter >= story.getChapters().size())
595 disabledRight = " disabled='disbaled'";
596 } else {
597 first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
598 previous = WebLibraryUrls.getViewUrl(luid, chapter,
599 (Math.max(paragraph - 1, 1)));
600 next = WebLibraryUrls.getViewUrl(luid, chapter,
601 (Math.min(paragraph + 1, chap.getParagraphs().size())));
602 last = WebLibraryUrls.getViewUrl(luid, chapter,
603 chap.getParagraphs().size());
604
605 if (paragraph <= 1)
606 disabledLeft = " disabled='disbaled'";
607 if (paragraph >= chap.getParagraphs().size())
608 disabledRight = " disabled='disbaled'";
609
610 // First -> previous *chapter*
611 if (chapter > 0)
612 disabledLeft = "";
613 first = WebLibraryUrls.getViewUrl(luid,
614 (Math.max(chapter - 1, 0)), null);
615 if (paragraph <= 1) {
616 previous = first;
617 }
618
619 Paragraph para = null;
620 try {
621 para = chap.getParagraphs().get(paragraph - 1);
622 } catch (IndexOutOfBoundsException e) {
623 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
624 NanoHTTPD.MIME_PLAINTEXT,
625 "Paragraph " + paragraph + " not found");
626 }
627
628 if (para.getType() == ParagraphType.IMAGE) {
629 String zoomStyle = "max-width: 100%;";
630 disabledZoomWidth = " disabled='disabled'";
631 String zoomOption = cookies.get("zoom");
632 if (zoomOption != null && !zoomOption.isEmpty()) {
633 if (zoomOption.equals("real")) {
634 zoomStyle = "";
635 disabledZoomWidth = "";
636 disabledZoomReal = " disabled='disabled'";
637 } else if (zoomOption.equals("width")) {
638 zoomStyle = "max-width: 100%;";
639 } else if (zoomOption.equals("height")) {
640 // see height of navbar + optionbar
641 zoomStyle = "max-height: calc(100% - 128px);";
642 disabledZoomWidth = "";
643 disabledZoomHeight = " disabled='disabled'";
644 }
645 }
646
647 String javascript = "document.getElementById(\"previous\").click(); return false;";
648 content.append(String.format("" //
649 + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
650 + "<img class='viewer img' style='%s' src='%s'/>"
651 + "</a>", //
652 javascript, //
653 next, //
654 zoomStyle, //
655 WebLibraryUrls.getStoryUrl(luid, chapter,
656 paragraph)));
657 } else {
658 content.append(String.format("" //
659 + "<div class='viewer text'>%s</div>", //
660 para.getContent()));
661 }
662 }
663
664 builder.append(String.format("" //
665 + "<div class='bar navbar'>\n" //
666 + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
667 + "\t<a%s id='previous' class='button previous' href='%s'>&lt;</a>\n" //
668 + "\t<div class='gotobox itemsbox'>\n" //
669 + "\t\t<div class='button goto'>%d</div>\n" //
670 + "\t\t<div class='items goto'>\n", //
671 disabledLeft, first, //
672 disabledLeft, previous, //
673 paragraph > 0 ? paragraph : chapter //
674 ));
675
676 // List of chap/para links
677
678 appendItemA(builder, 3, WebLibraryUrls.getViewUrl(luid, 0, null),
679 "Description", paragraph == 0 && chapter == 0);
680 if (paragraph > 0) {
681 for (int i = 1; i <= chap.getParagraphs().size(); i++) {
682 appendItemA(builder, 3,
683 WebLibraryUrls.getViewUrl(luid, chapter, i),
684 "Image " + i, paragraph == i);
685 }
686 } else {
687 int i = 1;
688 for (Chapter c : story.getChapters()) {
689 String chapName = "Chapter " + c.getNumber();
690 if (c.getName() != null && !c.getName().isEmpty()) {
691 chapName += ": " + c.getName();
692 }
693
694 appendItemA(builder, 3,
695 WebLibraryUrls.getViewUrl(luid, i, null), chapName,
696 chapter == i);
697
698 i++;
699 }
700 }
701
702 builder.append(String.format("" //
703 + "\t\t</div>\n" //
704 + "\t</div>\n" //
705 + "\t<a%s class='button next' href='%s'>&gt;</a>\n" //
706 + "\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
707 + "</div>\n", //
708 disabledRight, next, //
709 disabledRight, last //
710 ));
711
712 builder.append(content);
713
714 builder.append("<div class='bar optionbar ");
715 if (paragraph > 0) {
716 builder.append("s4");
717 } else {
718 builder.append("s1");
719 }
720 builder.append("'>\n");
721 builder.append(" <a class='button back' href='/'>BACK</a>\n");
722
723 if (paragraph > 0) {
724 builder.append(String.format("" //
725 + "\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
726 + "\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
727 + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
728 + "</div>\n", //
729 disabledZoomReal,
730 uri + "?optionName=zoom&optionValue=real", //
731 disabledZoomWidth,
732 uri + "?optionName=zoom&optionValue=width", //
733 disabledZoomHeight,
734 uri + "?optionName=zoom&optionValue=height" //
735 ));
736 }
737
738 appendPostHtml(builder);
739 return NanoHTTPD.newFixedLengthResponse(Status.OK,
740 NanoHTTPD.MIME_HTML, builder.toString());
741 } catch (IOException e) {
742 Instance.getInstance().getTraceHandler()
743 .error(new IOException("Cannot get image: " + uri, e));
744 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
745 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
746 }
747 }
748
749 protected Response newInputStreamResponse(String mimeType, InputStream in) {
750 if (in == null) {
751 return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
752 null);
753 }
754 return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
755 }
756
757 private String getContentOf(String file) {
758 InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
759 file);
760 if (in != null) {
761 try {
762 return IOUtils.readSmallStream(in);
763 } catch (IOException e) {
764 Instance.getInstance().getTraceHandler().error(
765 new IOException("Cannot get file: index.pre.html", e));
766 }
767 }
768
769 return "";
770 }
771
772 private void appendPreHtml(StringBuilder builder, boolean banner) {
773 String favicon = "favicon.ico";
774 String icon = Instance.getInstance().getUiConfig()
775 .getString(UiConfig.PROGRAM_ICON);
776 if (icon != null) {
777 favicon = "icon_" + icon.replace("-", "_") + ".png";
778 }
779
780 builder.append(
781 getContentOf("index.pre.html").replace("favicon.ico", favicon));
782
783 if (banner) {
784 builder.append("<div class='banner'>\n");
785 builder.append("\t<img class='ico' src='/") //
786 .append(favicon) //
787 .append("'/>\n");
788 builder.append("\t<h1>Fanfix</h1>\n");
789 builder.append("\t<h2>") //
790 .append(Version.getCurrentVersion()) //
791 .append("</h2>\n");
792 builder.append("</div>\n");
793 }
794 }
795
796 private void appendPostHtml(StringBuilder builder) {
797 builder.append(getContentOf("index.post.html"));
798 }
799
800 private void appendOption(StringBuilder builder, int depth, String name,
801 String value, String selected) {
802 for (int i = 0; i < depth; i++) {
803 builder.append("\t");
804 }
805 builder.append("<option value='").append(value).append("'");
806 if (value.equals(selected)) {
807 builder.append(" selected='selected'");
808 }
809 builder.append(">").append(name).append("</option>\n");
810 }
811
812 private void appendTableRow(StringBuilder builder, int depth,
813 String... tds) {
814 for (int i = 0; i < depth; i++) {
815 builder.append("\t");
816 }
817
818 int col = 1;
819 builder.append("<tr>");
820 for (String td : tds) {
821 builder.append("<td class='col");
822 builder.append(col++);
823 builder.append("'>");
824 builder.append(td);
825 builder.append("</td>");
826 }
827 builder.append("</tr>\n");
828 }
829
830 private void appendItemA(StringBuilder builder, int depth, String link,
831 String name, boolean selected) {
832 for (int i = 0; i < depth; i++) {
833 builder.append("\t");
834 }
835
836 builder.append("<a href='");
837 builder.append(link);
838 builder.append("' class='item goto");
839 if (selected) {
840 builder.append(" selected");
841 }
842 builder.append("'>");
843 builder.append(name);
844 builder.append("</a>\n");
845 }
846}