- [x] Make it run when no args passed
- [x] Fix the UI, it is ugly
- [x] Work on the UI thread is BAD
- - [ ] Allow export
+ - [x] Allow export
- [x] Allow delete/refresh
- [ ] Show a list of types
- [x] ..in the menu
- [x] Use it for most user ouput
- [ ] Use it for all user output
- [ ] French translation
-- [ ] Allow lauching a custom application instead of Desktop.start ?
+- [x] Allow lauching a custom application instead of Desktop.start ?
- [ ] Make a wrapper for firefox to create a new, empty profile ?
- [x] Install a mechanism to handle stories import/export progress update
- [x] Progress system
* <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
* {@link String}s when possible</li>
* <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
- * before taking the included ones; they will also be saved/updated into
- * this path when the program starts</li>
+ * before taking the usual ones; they will also be saved/updated into this
+ * path when the program starts</li>
* <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
* configuration value with 'true'</li>
* </ul>
public enum Config {
@Meta(what = "language", where = "", format = "language (example: en-GB) or nothing for default system language", info = "Force the language (can be overwritten again with the env variable $LANG)")
LANG, //
- @Meta(what = "reader type", where = "", format = "CLI or LOCAL", info = "Select the reader to use to read stories (CLI = simple output to console, LOCAL = use local system file handler)")
+ @Meta(what = "reader type", where = "", format = "CLI or LOCAL", info = "Select the default reader to use to read stories (CLI = simple output to console, LOCAL = use local system file handler)")
READER_TYPE, //
@Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)")
CACHE_DIR, //
@Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to store temporary files, defaults to directory 'tmp.reader' in the conig directory (usually $HOME/.fanfix)")
CACHE_DIR_LOCAL_READER, //
@Meta(what = "Output type", where = "Local Reader", format = "One of the known output type", info = "The type of output for the Local Reader for non-images documents")
- LOCAL_READER_NON_IMAGES_DOCUMENT_TYPE, //
+ NON_IMAGES_DOCUMENT_TYPE, //
@Meta(what = "Output type", where = "Local Reader", format = "One of the known output type", info = "The type of output for the Local Reader for images documents")
- LOCAL_READER_IMAGES_DOCUMENT_TYPE, //
+ IMAGES_DOCUMENT_TYPE, //
+ @Meta(what = "Program", where = "Local Reader", format = "A command to start", info = "The command launched for images documents -- default to the system default for the current file type")
+ IMAGES_DOCUMENT_READER, //
+ @Meta(what = "Program", where = "Local Reader", format = "A command to start", info = "The command launched for non images documents -- default to the system default for the current file type")
+ NON_IMAGES_DOCUMENT_READER, //
@Meta(what = "A background colour", where = "Local Reader Frame", format = "#rrggbb", info = "The background colour if you don't want the default system one")
BACKGROUND_COLOR, //
}
# Force the language (can be overwritten again with the env variable $LANG)
LANG =
# (WHAT: reader type, FORMAT: CLI or LOCAL)
-# Select the reader to use to read stories (CLI = simple output to console, LOCAL = use local system file handler)
+# Select the default reader to use to read stories (CLI = simple output to console, LOCAL = use local system file handler)
READER_TYPE =
# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
# The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)
CACHE_MAX_TIME_STABLE =
# (WHAT: string)
# The user-agent to use to download files
-USER_AGENT = Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0 -- ELinks/0.9.3 (Linux 2.6.11 i686; 79x24)
+USER_AGENT = Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0 -- ELinks/0.9.3 (Linux 2.6.11 i686; 80x24)
# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
# The directory where to get the default story covers
DEFAULT_COVERS_DIR = $HOME/bin/epub/
# (WHAT: help message, WHERE: cli, FORMAT: %s = supported input, %s = supported output)
# help message for the syntax
HELP_SYNTAX = Valid options:\n\
-t--import [URL]: import into library\n\
-t--export [id] [output_type] [target]: export story to target\n\
-t--convert [URL] [output_type] [target] (+info): convert URL into target\n\
-t--read [id] ([chapter number]): read the given story from the library\n\
-t--read-url [URL] ([chapter number]): convert on the fly and read the story, without saving it\n\
-t--list: list the stories present in the library\n\
-t--set-reader [reader type]: set the reader type to CLI or LOCAL for this command\n\
-t--help: this help message\n\
+\t--import [URL]: import into library\n\
+\t--export [id] [output_type] [target]: export story to target\n\
+\t--convert [URL] [output_type] [target] (+info): convert URL into target\n\
+\t--read [id] ([chapter number]): read the given story from the library\n\
+\t--read-url [URL] ([chapter number]): convert on the fly and read the story, without saving it\n\
+\t--list: list the stories present in the library\n\
+\t--set-reader [reader type]: set the reader type to CLI or LOCAL for this command\n\
+\t--help: this help message\n\
\n\
Supported input types:\n\
%s\n\
# (WHAT: input format description, WHERE: SupportType)
# Description of this input type
INPUT_DESC_TEXT = Support class for local stories encoded in textual format, with a few rules :\n\
-tthe title must be on the first line, \n\
-tthe author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), \n\
-tchapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number,\n\
-ta description of the story must be given as chapter number 0,\n\
-ta cover image may be present with the same filename but a PNG, JPEG or JPG extension.
+\tthe title must be on the first line, \n\
+\tthe author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), \n\
+\tchapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number,\n\
+\ta description of the story must be given as chapter number 0,\n\
+\ta cover image may be present with the same filename but a PNG, JPEG or JPG extension.
# (WHAT: input format description, WHERE: SupportType)
# Description of this input type
INPUT_DESC_INFO_TEXT = Contains the same information as the TEXT format, but with a companion ".info" file to store some metadata
# (WHAT: output format description, WHERE: OutputType)
# Description of this output type
OUTPUT_DESC_TEXT = Local stories encoded in textual format, with a few rules :\n\
-tthe title must be on the first line, \n\
-tthe author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), \n\
-tchapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number,\n\
-ta description of the story must be given as chapter number 0,\n\
-ta cover image may be present with the same filename but a PNG, JPEG or JPG extension.
+\tthe title must be on the first line, \n\
+\tthe author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), \n\
+\tchapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number,\n\
+\ta description of the story must be given as chapter number 0,\n\
+\ta cover image may be present with the same filename but a PNG, JPEG or JPG extension.
# (WHAT: output format description, WHERE: OutputType)
# Description of this output type
OUTPUT_DESC_INFO_TEXT = Contains the same information as the TEXT format, but with a companion ".info" file to store some metadata
CACHE_DIR_LOCAL_READER =
# (WHAT: Output type, WHERE: Local Reader, FORMAT: One of the known output type)
# The type of output for the Local Reader for non-images documents
-LOCAL_READER_NON_IMAGES_DOCUMENT_TYPE = HTML
+NON_IMAGES_DOCUMENT_TYPE = HTML
# (WHAT: Output type, WHERE: Local Reader, FORMAT: One of the known output type)
# The type of output for the Local Reader for images documents
-LOCAL_READER_IMAGES_DOCUMENT_TYPE = CBZ
+IMAGES_DOCUMENT_TYPE = CBZ
+# (WHAT: Program, WHERE: Local Reader, FORMAT: A command to start)
+# The command launched for images documents -- default to the system default for the current file type
+IMAGES_DOCUMENT_READER =
+# (WHAT: Program, WHERE: Local Reader, FORMAT: A command to start)
+# The command launched for non images documents -- default to the system default for the current file type
+NON_IMAGES_DOCUMENT_READER =
# (WHAT: A background colour, WHERE: Local Reader Frame, FORMAT: #rrggbb)
# The background colour if you don't want the default system one
BACKGROUND_COLOR = #FFFFFF
*
* @param chapter
* the chapter
+ *
+ * @throws IOException
+ * in case of I/O error or if the {@link Story} was not
+ * previously set
*/
- public abstract void read(int chapter);
+ public abstract void read(int chapter) throws IOException;
/**
* Start the reader in browse mode for the given type (or pass NULL for all
}
@Override
- public void read(int chapter) {
+ public void read(int chapter) throws IOException {
+ if (getStory() == null) {
+ throw new IOException("No story to read");
+ }
+
if (chapter > getStory().getChapters().size()) {
System.err.println("Chapter " + chapter + ": no such chapter");
} else {
package be.nikiroo.fanfix.reader;
+import java.awt.Desktop;
import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import be.nikiroo.fanfix.Instance;
import be.nikiroo.fanfix.Library;
import be.nikiroo.fanfix.bundles.UiConfig;
+import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.data.Story;
import be.nikiroo.fanfix.output.BasicOutput.OutputType;
import be.nikiroo.utils.Progress;
"Cannote create cache directory for local reader: " + dir);
}
- // TODO: can throw an exception, manage that (convert to IOEx ?)
- OutputType text = OutputType.valueOfNullOkUC(Instance.getUiConfig()
- .getString(UiConfig.LOCAL_READER_NON_IMAGES_DOCUMENT_TYPE));
- if (text == null) {
- text = OutputType.HTML;
- }
+ OutputType text = null;
+ OutputType images = null;
+
+ try {
+ text = OutputType.valueOfNullOkUC(Instance.getUiConfig().getString(
+ UiConfig.NON_IMAGES_DOCUMENT_TYPE));
+ if (text == null) {
+ text = OutputType.HTML;
+ }
+
+ images = OutputType.valueOfNullOkUC(Instance.getUiConfig()
+ .getString(UiConfig.IMAGES_DOCUMENT_TYPE));
+ if (images == null) {
+ images = OutputType.CBZ;
+ }
+ } catch (Exception e) {
+ UiConfig key = (text == null) ? UiConfig.NON_IMAGES_DOCUMENT_TYPE
+ : UiConfig.IMAGES_DOCUMENT_TYPE;
+ String value = Instance.getUiConfig().getString(key);
- OutputType images = OutputType.valueOfNullOkUC(Instance.getUiConfig()
- .getString(UiConfig.LOCAL_READER_IMAGES_DOCUMENT_TYPE));
- if (images == null) {
- images = OutputType.CBZ;
+ throw new IOException(
+ String.format(
+ "The configuration option %s is not valid: %s",
+ key, value), e);
}
- //
lib = new Library(dir, text, images);
}
@Override
public void read() throws IOException {
+ if (getStory() == null) {
+ throw new IOException("No story to read");
+ }
+
+ open(getStory().getMeta().getLuid(), null);
}
@Override
- public void read(int chapter) {
+ public void read(int chapter) throws IOException {
+ // TODO: show a special page?
+ read();
}
/**
});
}
+ // refresh = delete from LocalReader cache (TODO: rename?)
void refresh(String luid) {
lib.delete(luid);
}
+ // delete from main library
void delete(String luid) {
lib.delete(luid);
Instance.getLibrary().delete(luid);
}
+
+ // open the given book
+ void open(String luid, Progress pg) throws IOException {
+ MetaData meta = Instance.getLibrary().getInfo(luid);
+ File target = getTarget(luid, pg);
+
+ String program = null;
+ if (meta.isImageDocument()) {
+ program = Instance.getUiConfig().getString(
+ UiConfig.IMAGES_DOCUMENT_READER);
+ } else {
+ program = Instance.getUiConfig().getString(
+ UiConfig.NON_IMAGES_DOCUMENT_READER);
+ }
+
+ if (program != null && program.trim().isEmpty()) {
+ program = null;
+ }
+
+ if (program == null) {
+ try {
+ Desktop.getDesktop().browse(target.toURI());
+ } catch (UnsupportedOperationException e) {
+ Runtime.getRuntime().exec(
+ new String[] { "xdg-open", target.getAbsolutePath() });
+
+ }
+ } else {
+ Runtime.getRuntime().exec(
+ new String[] { program, target.getAbsolutePath() });
+
+ }
+ }
}
private boolean cached;
/**
- * Create a new {@link LocalReaderBook} item for the givn {@link Story}.
+ * Create a new {@link LocalReaderBook} item for the given {@link Story}.
*
* @param meta
* the story {@code}link MetaData}
* TRUE if it is locally cached
*/
public LocalReaderBook(MetaData meta, boolean cached) {
- this.luid = meta.getLuid();
this.cached = cached;
-
- BufferedImage resizedImage = new BufferedImage(SPINE_WIDTH
- + COVER_WIDTH, SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
- BufferedImage.TYPE_4BYTE_ABGR);
- Graphics2D g = resizedImage.createGraphics();
- g.setColor(Color.white);
- g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
- if (meta.getCover() != null) {
- g.drawImage(meta.getCover(), 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT,
- null);
- } else {
- g.setColor(Color.black);
- g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT);
- g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT);
- }
- g.dispose();
-
- icon = new JLabel(new ImageIcon(resizedImage));
+ luid = meta.getLuid();
String optAuthor = meta.getAuthor();
if (optAuthor != null && !optAuthor.isEmpty()) {
optAuthor = "(" + optAuthor + ")";
}
+ icon = new JLabel(generateCoverIcon(meta.getCover()));
+
title = new JLabel(
String.format(
"<html>"
TEXT_WIDTH, TEXT_HEIGHT, meta.getTitle(), AUTHOR_COLOR,
optAuthor));
- this.setLayout(new BorderLayout(10, 10));
- this.add(icon, BorderLayout.CENTER);
- this.add(title, BorderLayout.SOUTH);
+ setLayout(new BorderLayout(10, 10));
+ add(icon, BorderLayout.CENTER);
+ add(title, BorderLayout.SOUTH);
setupListeners();
- setSelected(false);
}
/**
* TRUE if it is selected
*/
public void setSelected(boolean selected) {
- this.selected = selected;
- repaint();
+ if (this.selected != selected) {
+ this.selected = selected;
+ repaint();
+ }
}
/**
* TRUE if it is mouse-hovered
*/
private void setHovered(boolean hovered) {
- this.hovered = hovered;
- repaint();
+ if (this.hovered != hovered) {
+ this.hovered = hovered;
+ repaint();
+ }
}
/**
public void setCached(boolean cached) {
if (this.cached != cached) {
this.cached = cached;
- invalidate();
+ repaint();
}
}
/**
- * Draw a "cached" icon and a partially transparent overlay if needed
- * depending upon the selection and mouse-hover states on top of the normal
- * component.
+ * Paint the item, then call {@link LocalReaderBook#paintOverlay(Graphics)}.
*/
@Override
public void paint(Graphics g) {
super.paint(g);
+ paintOverlay(g);
+ }
+ /**
+ * Draw a partially transparent overlay if needed depending upon the
+ * selection and mouse-hover states on top of the normal component, as well
+ * as a possible "cached" icon if the item is cached.
+ */
+ public void paintOverlay(Graphics g) {
Rectangle clip = g.getClipBounds();
if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
return;
+ HOFFSET + 30, 10, 20, 20);
}
}
+
+ /**
+ * Generate a cover icon based upon the given cover image (which may be
+ * NULL).
+ *
+ * @param image
+ * the cover image, or NULL for none
+ *
+ * @return the icon
+ */
+ private ImageIcon generateCoverIcon(BufferedImage image) {
+ BufferedImage resizedImage = new BufferedImage(SPINE_WIDTH
+ + COVER_WIDTH, SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
+ BufferedImage.TYPE_4BYTE_ABGR);
+ Graphics2D g = resizedImage.createGraphics();
+ g.setColor(Color.white);
+ g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
+ if (image != null) {
+ g.drawImage(image, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT, null);
+ } else {
+ g.setColor(Color.black);
+ g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT);
+ g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT);
+ }
+ g.dispose();
+
+ return new ImageIcon(resizedImage);
+ }
}
import java.awt.BorderLayout;
import java.awt.Color;
-import java.awt.Desktop;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
this.type = type;
stories = Instance.getLibrary().getList(type);
books.clear();
+ bookPane.invalidate();
bookPane.removeAll();
for (MetaData meta : stories) {
LocalReaderBook book = new LocalReaderBook(meta,
List<String> tt = Instance.getLibrary().getTypes();
tt.add(0, null);
for (final String type : tt) {
-
JMenuItem item = new JMenuItem(type == null ? "All books" : type);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
return export;
}
+ /**
+ * Create a {@link FileFilter} that accepts all files and return the given
+ * description.
+ *
+ * @param desc
+ * the description
+ *
+ * @return the filter
+ */
private FileFilter createAllFilter(final String desc) {
return new FileFilter() {
@Override
outOfUi(pg, new Runnable() {
public void run() {
try {
- File target = LocalReaderFrame.this.reader.getTarget(
- book.getLuid(), pg);
- book.setCached(true);
- // TODO: allow custom programs, with
- // Desktop/xdg-open fallback
- try {
- Desktop.getDesktop().browse(target.toURI());
- } catch (UnsupportedOperationException e) {
- String browsers[] = new String[] { "xdg-open",
- "epiphany", "konqueror", "firefox", "chrome",
- "google-chrome", "mozilla" };
-
- Runtime runtime = Runtime.getRuntime();
- for (String browser : browsers) {
- try {
- runtime.exec(new String[] { browser,
- target.getAbsolutePath() });
- runtime = null;
- break;
- } catch (IOException ioe) {
- // continue, try next browser
- }
- }
-
- if (runtime != null) {
- throw new IOException(
- "Cannot find a working GUI browser...");
+ reader.open(book.getLuid(), pg);
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ book.setCached(true);
}
- }
+ });
} catch (IOException e) {
+ // TODO: error message?
Instance.syserr(e);
}
}
"Cannot import: " + url,
e.getMessage(),
JOptionPane.ERROR_MESSAGE);
-
- setEnabled(true);
} else {
refreshBooks(type);
}
public void setEnabled(boolean b) {
for (LocalReaderBook book : books) {
book.setEnabled(b);
- book.validate();
book.repaint();
}
bar.setEnabled(b);
bookPane.setEnabled(b);
- bookPane.validate();
bookPane.repaint();
super.setEnabled(b);
- validate();
repaint();
}
}