1 package be
.nikiroo
.fanfix
.reader
.ui
;
3 import java
.awt
.BorderLayout
;
5 import java
.awt
.Graphics
;
6 import java
.awt
.Graphics2D
;
7 import java
.awt
.Polygon
;
8 import java
.awt
.Rectangle
;
9 import java
.awt
.event
.MouseEvent
;
10 import java
.awt
.event
.MouseListener
;
11 import java
.awt
.image
.BufferedImage
;
12 import java
.io
.ByteArrayInputStream
;
13 import java
.io
.ByteArrayOutputStream
;
14 import java
.io
.IOException
;
15 import java
.io
.InputStream
;
16 import java
.net
.MalformedURLException
;
17 import java
.util
.ArrayList
;
18 import java
.util
.Date
;
19 import java
.util
.EventListener
;
20 import java
.util
.List
;
22 import javax
.imageio
.ImageIO
;
23 import javax
.swing
.ImageIcon
;
24 import javax
.swing
.JLabel
;
25 import javax
.swing
.JPanel
;
27 import be
.nikiroo
.fanfix
.Instance
;
28 import be
.nikiroo
.fanfix
.data
.MetaData
;
29 import be
.nikiroo
.fanfix
.data
.Story
;
30 import be
.nikiroo
.fanfix
.reader
.Reader
;
31 import be
.nikiroo
.utils
.Image
;
32 import be
.nikiroo
.utils
.ui
.ImageUtilsAwt
;
33 import be
.nikiroo
.utils
.ui
.UIUtils
;
36 * A book item presented in a {@link GuiReaderFrame}.
40 class GuiReaderBook
extends JPanel
{
42 * Action on a book item.
46 interface BookActionListener
extends EventListener
{
48 * The book was selected (single click).
51 * the {@link GuiReaderBook} itself
53 public void select(GuiReaderBook book
);
56 * The book was double-clicked.
59 * the {@link GuiReaderBook} itself
61 public void action(GuiReaderBook book
);
64 * A popup menu was requested for this {@link GuiReaderBook}.
67 * the {@link GuiReaderBook} itself
69 * the {@link MouseEvent} that generated this call
71 public void popupRequested(GuiReaderBook book
, MouseEvent e
);
74 private static final long serialVersionUID
= 1L;
76 // TODO: export some of the configuration options?
77 private static final int COVER_WIDTH
= 100;
78 private static final int COVER_HEIGHT
= 150;
79 private static final int SPINE_WIDTH
= 5;
80 private static final int SPINE_HEIGHT
= 5;
81 private static final int HOFFSET
= 20;
82 private static final Color SPINE_COLOR_BOTTOM
= new Color(180, 180, 180);
83 private static final Color SPINE_COLOR_RIGHT
= new Color(100, 100, 100);
84 private static final int TEXT_WIDTH
= COVER_WIDTH
+ 40;
85 private static final int TEXT_HEIGHT
= 50;
86 private static final String AUTHOR_COLOR
= "#888888";
87 private static final Color BORDER
= Color
.black
;
88 private static final long doubleClickDelay
= 200; // in ms
93 private boolean selected
;
94 private boolean hovered
;
95 private Date lastClick
;
97 private List
<BookActionListener
> listeners
;
98 private Reader reader
;
99 private MetaData meta
;
100 private boolean cached
;
103 * Create a new {@link GuiReaderBook} item for the given {@link Story}.
106 * the associated reader
108 * the story {@link MetaData} or source (if no LUID)
110 * TRUE if it is locally cached
111 * @param seeWordCount
112 * TRUE to see word counts, FALSE to see authors
114 public GuiReaderBook(Reader reader
, MetaData meta
, boolean cached
,
115 boolean seeWordCount
) {
116 this.reader
= reader
;
117 this.cached
= cached
;
120 String optSecondary
= meta
.getAuthor();
122 if (meta
.getWords() >= 4000) {
123 optSecondary
= "" + (meta
.getWords() / 1000) + "k";
124 } else if (meta
.getWords() > 0) {
125 optSecondary
= "" + meta
.getWords();
130 if (!optSecondary
.isEmpty()) {
131 if (meta
.isImageDocument()) {
132 optSecondary
+= " images";
134 optSecondary
+= " words";
139 if (optSecondary
!= null && !optSecondary
.isEmpty()) {
140 optSecondary
= "(" + optSecondary
+ ")";
145 icon
= new JLabel(generateCoverIcon());
149 + "<body style='width: %d px; height: %d px; text-align: center'>"
150 + "%s" + "<br>" + "<span style='color: %s;'>"
151 + "%s" + "</span>" + "</body>" + "</html>",
152 TEXT_WIDTH
, TEXT_HEIGHT
, meta
.getTitle(), AUTHOR_COLOR
,
155 setLayout(new BorderLayout(10, 10));
156 add(icon
, BorderLayout
.CENTER
);
157 add(title
, BorderLayout
.SOUTH
);
163 * The book current selection state.
165 * @return the selection state
167 public boolean isSelected() {
172 * The book current selection state.
175 * TRUE if it is selected
177 public void setSelected(boolean selected
) {
178 if (this.selected
!= selected
) {
179 this.selected
= selected
;
185 * The item mouse-hover state.
188 * TRUE if it is mouse-hovered
190 private void setHovered(boolean hovered
) {
191 if (this.hovered
!= hovered
) {
192 this.hovered
= hovered
;
198 * Setup the mouse listener that will activate {@link BookActionListener}
201 private void setupListeners() {
202 listeners
= new ArrayList
<GuiReaderBook
.BookActionListener
>();
203 addMouseListener(new MouseListener() {
205 public void mouseReleased(MouseEvent e
) {
206 if (e
.isPopupTrigger()) {
212 public void mousePressed(MouseEvent e
) {
213 if (e
.isPopupTrigger()) {
219 public void mouseExited(MouseEvent e
) {
224 public void mouseEntered(MouseEvent e
) {
229 public void mouseClicked(MouseEvent e
) {
231 Date now
= new Date();
232 if (lastClick
!= null
233 && now
.getTime() - lastClick
.getTime() < doubleClickDelay
) {
243 private void click(boolean doubleClick
) {
244 for (BookActionListener listener
: listeners
) {
246 listener
.action(GuiReaderBook
.this);
248 listener
.select(GuiReaderBook
.this);
253 private void popup(MouseEvent e
) {
254 for (BookActionListener listener
: listeners
) {
255 listener
.select((GuiReaderBook
.this));
256 listener
.popupRequested(GuiReaderBook
.this, e
);
263 * Add a new {@link BookActionListener} on this item.
268 public void addActionListener(BookActionListener listener
) {
269 listeners
.add(listener
);
273 * The Library {@link MetaData} of the book represented by this item.
277 public MetaData
getMeta() {
282 * This item {@link GuiReader} library cache state.
284 * @return TRUE if it is present in the {@link GuiReader} cache
286 public boolean isCached() {
291 * This item {@link GuiReader} library cache state.
294 * TRUE if it is present in the {@link GuiReader} cache
296 public void setCached(boolean cached
) {
297 if (this.cached
!= cached
) {
298 this.cached
= cached
;
304 * Paint the item, then call {@link GuiReaderBook#paintOverlay(Graphics)}.
307 public void paint(Graphics g
) {
313 * Draw a partially transparent overlay if needed depending upon the
314 * selection and mouse-hover states on top of the normal component, as well
315 * as a possible "cached" icon if the item is cached.
318 * the {@link Graphics} to paint onto
320 public void paintOverlay(Graphics g
) {
321 Rectangle clip
= g
.getClipBounds();
322 if (clip
.getWidth() <= 0 || clip
.getHeight() <= 0) {
326 int h
= COVER_HEIGHT
;
328 int xOffset
= (TEXT_WIDTH
- COVER_WIDTH
) - 1;
329 int yOffset
= HOFFSET
;
331 if (BORDER
!= null) {
332 if (BORDER
!= null) {
334 g
.drawRect(xOffset
, yOffset
, COVER_WIDTH
, COVER_HEIGHT
);
341 int[] xs
= new int[] { xOffset
, xOffset
+ SPINE_WIDTH
,
342 xOffset
+ w
+ SPINE_WIDTH
, xOffset
+ w
};
343 int[] ys
= new int[] { yOffset
+ h
, yOffset
+ h
+ SPINE_HEIGHT
,
344 yOffset
+ h
+ SPINE_HEIGHT
, yOffset
+ h
};
345 g
.setColor(SPINE_COLOR_BOTTOM
);
346 g
.fillPolygon(new Polygon(xs
, ys
, xs
.length
));
347 xs
= new int[] { xOffset
+ w
, xOffset
+ w
+ SPINE_WIDTH
,
348 xOffset
+ w
+ SPINE_WIDTH
, xOffset
+ w
};
349 ys
= new int[] { yOffset
, yOffset
+ SPINE_HEIGHT
,
350 yOffset
+ h
+ SPINE_HEIGHT
, yOffset
+ h
};
351 g
.setColor(SPINE_COLOR_RIGHT
);
352 g
.fillPolygon(new Polygon(xs
, ys
, xs
.length
));
354 Color color
= new Color(255, 255, 255, 0);
356 } else if (selected
&& !hovered
) {
357 color
= new Color(80, 80, 100, 40);
358 } else if (!selected
&& hovered
) {
359 color
= new Color(230, 230, 255, 100);
360 } else if (selected
&& hovered
) {
361 color
= new Color(200, 200, 255, 100);
365 g
.fillRect(clip
.x
, clip
.y
, clip
.width
, clip
.height
);
368 UIUtils
.drawEllipse3D(g
, Color
.green
.darker(), COVER_WIDTH
369 + HOFFSET
+ 30, 10, 20, 20);
374 * Generate a cover icon based upon the given {@link MetaData}.
378 private ImageIcon
generateCoverIcon() {
379 BufferedImage resizedImage
= null;
380 String id
= getIconId(meta
);
382 InputStream in
= Instance
.getCache().getFromCache(id
);
385 resizedImage
= ImageUtilsAwt
.fromImage(new Image(in
));
388 } catch (IOException e
) {
389 Instance
.getTraceHandler().error(e
);
393 if (resizedImage
== null) {
396 if (meta
.getLuid() == null) {
397 cover
= reader
.getLibrary()
398 .getSourceCover(meta
.getSource());
400 cover
= reader
.getLibrary().getCover(meta
.getLuid());
403 BufferedImage coverb
= ImageUtilsAwt
.fromImage(cover
);
405 resizedImage
= new BufferedImage(SPINE_WIDTH
+ COVER_WIDTH
,
406 SPINE_HEIGHT
+ COVER_HEIGHT
+ HOFFSET
,
407 BufferedImage
.TYPE_4BYTE_ABGR
);
408 Graphics2D g
= resizedImage
.createGraphics();
409 g
.setColor(Color
.white
);
410 g
.fillRect(0, HOFFSET
, COVER_WIDTH
, COVER_HEIGHT
);
412 g
.drawImage(coverb
, 0, HOFFSET
, COVER_WIDTH
, COVER_HEIGHT
,
415 g
.setColor(Color
.black
);
416 g
.drawLine(0, HOFFSET
, COVER_WIDTH
, HOFFSET
+ COVER_HEIGHT
);
417 g
.drawLine(COVER_WIDTH
, HOFFSET
, 0, HOFFSET
+ COVER_HEIGHT
);
422 ByteArrayOutputStream out
= new ByteArrayOutputStream();
423 ImageIO
.write(resizedImage
, "png", out
);
424 byte[] imageBytes
= out
.toByteArray();
425 in
= new ByteArrayInputStream(imageBytes
);
426 Instance
.getCache().addToCache(in
, id
);
430 } catch (MalformedURLException e
) {
431 Instance
.getTraceHandler().error(e
);
432 } catch (IOException e
) {
433 Instance
.getTraceHandler().error(e
);
437 if (resizedImage
!= null) {
438 return new ImageIcon(resizedImage
);
445 * Manually clear the icon set for this item.
448 * the meta of the story or source (if luid is null)
450 public static void clearIcon(MetaData meta
) {
451 String id
= getIconId(meta
);
452 Instance
.getCache().removeFromCache(id
);
456 * Get a unique ID from this meta (note that if the luid is null, it is
457 * considered a source and not a {@link Story}).
461 * @return the unique ID
463 private static String
getIconId(MetaData meta
) {
466 String key
= meta
.getUuid();
467 if (meta
.getLuid() == null) {
468 // a fake meta (== a source)
469 key
= "source_" + meta
.getSource();
472 id
= key
+ ".thumb_" + SPINE_WIDTH
+ "x" + COVER_WIDTH
+ "+"
473 + SPINE_HEIGHT
+ "+" + COVER_HEIGHT
+ "@" + HOFFSET
;