1 package be
.nikiroo
.fanfix
.reader
;
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
.utils
.IOUtils
;
31 import be
.nikiroo
.utils
.ui
.UIUtils
;
34 * A book item presented in a {@link LocalReaderFrame}.
38 class LocalReaderBook
extends JPanel
{
40 * Action on a book item.
44 interface BookActionListener
extends EventListener
{
46 * The book was selected (single click).
49 * the {@link LocalReaderBook} itself
51 public void select(LocalReaderBook book
);
54 * The book was double-clicked.
57 * the {@link LocalReaderBook} itself
59 public void action(LocalReaderBook book
);
62 * A popup menu was requested for this {@link LocalReaderBook}.
65 * the {@link LocalReaderBook} itself
67 * the {@link MouseEvent} that generated this call
69 public void popupRequested(LocalReaderBook book
, MouseEvent e
);
72 private static final long serialVersionUID
= 1L;
74 // TODO: export some of the configuration options?
75 private static final int COVER_WIDTH
= 100;
76 private static final int COVER_HEIGHT
= 150;
77 private static final int SPINE_WIDTH
= 5;
78 private static final int SPINE_HEIGHT
= 5;
79 private static final int HOFFSET
= 20;
80 private static final Color SPINE_COLOR_BOTTOM
= new Color(180, 180, 180);
81 private static final Color SPINE_COLOR_RIGHT
= new Color(100, 100, 100);
82 private static final int TEXT_WIDTH
= COVER_WIDTH
+ 40;
83 private static final int TEXT_HEIGHT
= 50;
84 private static final String AUTHOR_COLOR
= "#888888";
85 private static final Color BORDER
= Color
.black
;
86 private static final long doubleClickDelay
= 200; // in ms
91 private boolean selected
;
92 private boolean hovered
;
93 private Date lastClick
;
95 private List
<BookActionListener
> listeners
;
96 private MetaData meta
;
97 private boolean cached
;
100 * Create a new {@link LocalReaderBook} item for the given {@link Story}.
103 * the story {@code}link MetaData}
105 * TRUE if it is locally cached
106 * @param seeWordcount
107 * TRUE to see word counts, FALSE to see authors
109 public LocalReaderBook(MetaData meta
, boolean cached
, boolean seeWordCount
) {
110 this.cached
= cached
;
113 String optSecondary
= meta
.getAuthor();
115 if (meta
.getWords() >= 4000) {
116 optSecondary
= (meta
.getWords() / 1000) + "k words";
117 } else if (meta
.getWords() > 0) {
118 optSecondary
= meta
.getWords() + " words";
124 if (optSecondary
!= null && !optSecondary
.isEmpty()) {
125 optSecondary
= "(" + optSecondary
+ ")";
128 icon
= new JLabel(generateCoverIcon(meta
));
132 + "<body style='width: %d px; height: %d px; text-align: center'>"
133 + "%s" + "<br>" + "<span style='color: %s;'>"
134 + "%s" + "</span>" + "</body>" + "</html>",
135 TEXT_WIDTH
, TEXT_HEIGHT
, meta
.getTitle(), AUTHOR_COLOR
,
138 setLayout(new BorderLayout(10, 10));
139 add(icon
, BorderLayout
.CENTER
);
140 add(title
, BorderLayout
.SOUTH
);
146 * The book current selection state.
148 * @return the selection state
150 public boolean isSelected() {
155 * The book current selection state.
158 * TRUE if it is selected
160 public void setSelected(boolean selected
) {
161 if (this.selected
!= selected
) {
162 this.selected
= selected
;
168 * The item mouse-hover state.
171 * TRUE if it is mouse-hovered
173 private void setHovered(boolean hovered
) {
174 if (this.hovered
!= hovered
) {
175 this.hovered
= hovered
;
181 * Setup the mouse listener that will activate {@link BookActionListener}
184 private void setupListeners() {
185 listeners
= new ArrayList
<LocalReaderBook
.BookActionListener
>();
186 addMouseListener(new MouseListener() {
187 public void mouseReleased(MouseEvent e
) {
188 if (e
.isPopupTrigger()) {
193 public void mousePressed(MouseEvent e
) {
194 if (e
.isPopupTrigger()) {
199 public void mouseExited(MouseEvent e
) {
203 public void mouseEntered(MouseEvent e
) {
207 public void mouseClicked(MouseEvent e
) {
209 Date now
= new Date();
210 if (lastClick
!= null
211 && now
.getTime() - lastClick
.getTime() < doubleClickDelay
) {
221 private void click(boolean doubleClick
) {
222 for (BookActionListener listener
: listeners
) {
224 listener
.action(LocalReaderBook
.this);
226 listener
.select(LocalReaderBook
.this);
231 private void popup(MouseEvent e
) {
232 for (BookActionListener listener
: listeners
) {
233 listener
.select((LocalReaderBook
.this));
234 listener
.popupRequested(LocalReaderBook
.this, e
);
241 * Add a new {@link BookActionListener} on this item.
246 public void addActionListener(BookActionListener listener
) {
247 listeners
.add(listener
);
251 * The Library {@code}link MetaData} of the book represented by this item.
255 public MetaData
getMeta() {
260 * This item {@link LocalReader} library cache state.
262 * @return TRUE if it is present in the {@link LocalReader} cache
264 public boolean isCached() {
269 * This item {@link LocalReader} library cache state.
272 * TRUE if it is present in the {@link LocalReader} cache
274 public void setCached(boolean cached
) {
275 if (this.cached
!= cached
) {
276 this.cached
= cached
;
282 * Paint the item, then call {@link LocalReaderBook#paintOverlay(Graphics)}.
285 public void paint(Graphics g
) {
291 * Draw a partially transparent overlay if needed depending upon the
292 * selection and mouse-hover states on top of the normal component, as well
293 * as a possible "cached" icon if the item is cached.
295 public void paintOverlay(Graphics g
) {
296 Rectangle clip
= g
.getClipBounds();
297 if (clip
.getWidth() <= 0 || clip
.getHeight() <= 0) {
301 int h
= COVER_HEIGHT
;
303 int xOffset
= (TEXT_WIDTH
- COVER_WIDTH
) - 1;
304 int yOffset
= HOFFSET
;
306 if (BORDER
!= null) {
307 if (BORDER
!= null) {
309 g
.drawRect(xOffset
, yOffset
, COVER_WIDTH
, COVER_HEIGHT
);
316 int[] xs
= new int[] { xOffset
, xOffset
+ SPINE_WIDTH
,
317 xOffset
+ w
+ SPINE_WIDTH
, xOffset
+ w
};
318 int[] ys
= new int[] { yOffset
+ h
, yOffset
+ h
+ SPINE_HEIGHT
,
319 yOffset
+ h
+ SPINE_HEIGHT
, yOffset
+ h
};
320 g
.setColor(SPINE_COLOR_BOTTOM
);
321 g
.fillPolygon(new Polygon(xs
, ys
, xs
.length
));
322 xs
= new int[] { xOffset
+ w
, xOffset
+ w
+ SPINE_WIDTH
,
323 xOffset
+ w
+ SPINE_WIDTH
, xOffset
+ w
};
324 ys
= new int[] { yOffset
, yOffset
+ SPINE_HEIGHT
,
325 yOffset
+ h
+ SPINE_HEIGHT
, yOffset
+ h
};
326 g
.setColor(SPINE_COLOR_RIGHT
);
327 g
.fillPolygon(new Polygon(xs
, ys
, xs
.length
));
329 Color color
= new Color(255, 255, 255, 0);
331 } else if (selected
&& !hovered
) {
332 color
= new Color(80, 80, 100, 40);
333 } else if (!selected
&& hovered
) {
334 color
= new Color(230, 230, 255, 100);
335 } else if (selected
&& hovered
) {
336 color
= new Color(200, 200, 255, 100);
340 g
.fillRect(clip
.x
, clip
.y
, clip
.width
, clip
.height
);
343 UIUtils
.drawEllipse3D(g
, Color
.green
.darker(), COVER_WIDTH
344 + HOFFSET
+ 30, 10, 20, 20);
349 * Generate a cover icon based upon the given {@link MetaData}.
352 * the {@link MetaData} about the target {@link Story}
356 private ImageIcon
generateCoverIcon(MetaData meta
) {
357 String id
= meta
.getUuid() + ".thumb_" + SPINE_WIDTH
+ "x"
358 + COVER_WIDTH
+ "+" + SPINE_HEIGHT
+ "+" + COVER_HEIGHT
+ "@"
360 BufferedImage resizedImage
= null;
362 InputStream in
= Instance
.getCache().getFromCache(id
);
365 resizedImage
= IOUtils
.toImage(in
);
368 } catch (IOException e
) {
373 if (resizedImage
== null) {
375 BufferedImage cover
= Instance
.getLibrary().getCover(
378 resizedImage
= new BufferedImage(SPINE_WIDTH
+ COVER_WIDTH
,
379 SPINE_HEIGHT
+ COVER_HEIGHT
+ HOFFSET
,
380 BufferedImage
.TYPE_4BYTE_ABGR
);
381 Graphics2D g
= resizedImage
.createGraphics();
382 g
.setColor(Color
.white
);
383 g
.fillRect(0, HOFFSET
, COVER_WIDTH
, COVER_HEIGHT
);
385 g
.drawImage(cover
, 0, HOFFSET
, COVER_WIDTH
, COVER_HEIGHT
,
388 g
.setColor(Color
.black
);
389 g
.drawLine(0, HOFFSET
, COVER_WIDTH
, HOFFSET
+ COVER_HEIGHT
);
390 g
.drawLine(COVER_WIDTH
, HOFFSET
, 0, HOFFSET
+ COVER_HEIGHT
);
394 ByteArrayOutputStream out
= new ByteArrayOutputStream();
395 ImageIO
.write(resizedImage
, "png", out
);
396 byte[] imageBytes
= out
.toByteArray();
397 in
= new ByteArrayInputStream(imageBytes
);
398 Instance
.getCache().addToCache(in
, id
);
401 } catch (MalformedURLException e
) {
403 } catch (IOException e
) {
408 return new ImageIcon(resizedImage
);