Fanfix is a small Java program that can download stories from some supported websites and render them offline.
+![Main GUI](screenshots/fanfix.png?raw=true "Main GUI")
+
It will convert from a (supported) URL to an .epub file for stories or a .cbz file for comics (a few other output types are also available, like Plain Text or LaTeX).
To help organize your stories, it can also work as a local library.
Any platform with at lest Java 1.6 on it should be ok.
-If you have any problems to compile it with a supported Java version (1.5 won't work, but you may try to cross-compile or change the Bundle.java class from the utilities; 1.6 and 1.8 have been tested and work), please contact me.
+It has only been tested on Linux and Windows for now, but feel free to inform me if you try it on another system.
+
+If you have any problems to compile it with a supported Java version (1.5 won't work, but you may try to cross-compile; 1.6 and 1.8 have been tested and work), please contact me.
## Usage
-You can start the program in CLI mode:
+You can start the program in GUI mode (as in the screenshot on top):
- ```java -jar fanfix.jar```
-__TODO__: offer a GUI mode (work in progress)
-The following arguments are allowed:
+The following arguments are also allowed:
- ```--import [URL]```: import the story at URL into the local library
- ```--export [id] [output_type] [target]```: export the story denoted by ID to the target file
- ```--convert [URL] [output_type] [target] (+info)```: convert the story at URL into target, and force-add the .info and cover if +info is passed
- [ ] A GUI (work in progress)
- [x] Make one
- [x] Make it run when no args passed
- - [ ] Fix the UI, it is ugly
+ - [x] Fix the UI, it is ugly
- [ ] Work on the UI thread is BAD
- [ ] Allow export
- [ ] Show a list of types
- [x] Make use of it
- [x] Use it for all user output (some WIP remains)
- [ ] French translation
-- [ ] Allow lauching a custom application instead of Desktop.star ?
+- [ ] Allow lauching a custom application instead of Desktop.start ?
- [ ] Make a wrapper for firefox to create a new, empty profile ?
+- [ ] Install a mechanism to handle stories import progress update
import be.nikiroo.fanfix.bundles.Config;
import be.nikiroo.fanfix.bundles.ConfigBundle;
import be.nikiroo.fanfix.bundles.StringIdBundle;
+import be.nikiroo.fanfix.bundles.UiConfig;
+import be.nikiroo.fanfix.bundles.UiConfigBundle;
import be.nikiroo.fanfix.output.BasicOutput.OutputType;
import be.nikiroo.utils.resources.Bundles;
*/
public class Instance {
private static ConfigBundle config;
+ private static UiConfigBundle uiconfig;
private static StringIdBundle trans;
private static Cache cache;
private static Library lib;
} catch (IOException e) {
syserr(e);
}
+ try {
+ uiconfig = new UiConfigBundle();
+ uiconfig.updateFile(configDir);
+ } catch (IOException e) {
+ syserr(e);
+ }
try {
trans = new StringIdBundle(getLang());
trans.updateFile(configDir);
Bundles.setDirectory(configDir);
}
+ uiconfig = new UiConfigBundle();
trans = new StringIdBundle(getLang());
try {
lib = new Library(getFile(Config.LIBRARY_DIR),
debug = Instance.getConfig().getBoolean(Config.DEBUG_ERR, false);
coverDir = getFile(Config.DEFAULT_COVERS_DIR);
File tmp = getFile(Config.CACHE_DIR);
- readerTmp = getFile(Config.CACHE_DIR_LOCAL_READER);
+ readerTmp = getFile(UiConfig.CACHE_DIR_LOCAL_READER);
if (checkEnv("NOUTF")) {
trans.setUnicode(false);
return config;
}
+ /**
+ * Get the (unique) UI configuration service for the program.
+ *
+ * @return the configuration service
+ */
+ public static UiConfigBundle getUiConfig() {
+ return uiconfig;
+ }
+
/**
* Get the (unique) {@link Cache} for the program.
*
* @return the path
*/
private static File getFile(Config id) {
+ return getFile(config.getString(id));
+ }
+
+ /**
+ * Return a path, but support the special $HOME variable.
+ *
+ * @return the path
+ */
+ private static File getFile(UiConfig id) {
+ return getFile(uiconfig.getString(id));
+ }
+
+ /**
+ * Return a path, but support the special $HOME variable.
+ *
+ * @return the path
+ */
+ private static File getFile(String path) {
File file = null;
- String path = config.getString(id);
if (path != null && !path.isEmpty()) {
path = path.replace('/', File.separatorChar);
if (path.contains("$HOME")) {
* @throws IOException
* in case of I/O error
*/
- private Story save(Story story, String luid) throws IOException {
+ public Story save(Story story, String luid) throws IOException {
// Do not change the original metadata, but change the original story
MetaData key = story.getMeta().clone();
story.setMeta(key);
import be.nikiroo.fanfix.reader.BasicReader.ReaderType;
import be.nikiroo.fanfix.supported.BasicSupport;
import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
+import be.nikiroo.utils.UIUtils;
/**
* Main program entry point.
case SET_READER:
break;
case START:
+ UIUtils.setLookAndFeel();
BasicReader.setDefaultReaderType(ReaderType.LOCAL);
BasicReader.getReader().start(null);
break;
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 a directory 'fanfic-tmp' in the system default temporary directory")
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 a directory 'fanfic-reader' in the system default temporary directory")
- CACHE_DIR_LOCAL_READER, //
@Meta(what = "delay in hours", where = "", format = "integer | 0: no cache | -1: infinite time cache which is default", info = "The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh")
CACHE_MAX_TIME_CHANGING, //
@Meta(what = "delay in hours", where = "", format = "integer | 0: no cache | -1: infinite time cache which is default", info = "The delay after which a cached resource that is thought to change rarely is considered too old and triggers a refresh")
CHAPTER_EN, //
@Meta(what = "Chapter identification string", where = "", format = "", info = "used to identify a starting chapter in text mode")
CHAPTER_FR, //
- @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, //
- @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, //
-
}
public enum Target {
/**
* Configuration options that the user can change in the
- * <tt>.properties</tt> file.
+ * <tt>.properties</tt> file
*/
config,
- /** Translation resources. */
+ /** Translation resources */
resources,
+ /** UI resources (from colours to behaviour) */
+ ui,
}
--- /dev/null
+package be.nikiroo.fanfix.bundles;
+
+import be.nikiroo.utils.resources.Meta;
+
+/**
+ * The configuration options.
+ *
+ * @author niki
+ */
+public enum UiConfig {
+ @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 a directory 'fanfic-reader' in the system default temporary directory")
+ 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, //
+ @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, //
+ @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, //
+}
--- /dev/null
+package be.nikiroo.fanfix.bundles;
+
+import java.io.File;
+import java.io.IOException;
+
+import be.nikiroo.utils.resources.Bundle;
+
+/**
+ * This class manages the configuration of UI of the application (colours and
+ * behaviour)
+ *
+ * @author niki
+ */
+public class UiConfigBundle extends Bundle<UiConfig> {
+ public UiConfigBundle() {
+ super(UiConfig.class, Target.ui);
+ }
+
+ /**
+ * Update resource file.
+ *
+ * @param args
+ * not used
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public static void main(String[] args) throws IOException {
+ String path = new File(".").getAbsolutePath()
+ + "/src/be/nikiroo/fanfix/bundles/";
+ new UiConfigBundle().updateFile(path);
+ System.out.println("Path updated: " + path);
+ }
+
+ @Override
+ protected String getBundleDisplayName() {
+ return "UI configuration options";
+ }
+}
# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
# The directory where to store temporary files, defaults to a directory 'fanfic-tmp' in the system default temporary directory
CACHE_DIR =
-# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
-# The directory where to store temporary files, defaults to a directory 'fanfic-reader' in the system default temporary directory
-CACHE_DIR_LOCAL_READER =
# (WHAT: delay in hours, FORMAT: integer | 0: no cache | -1: infinite time cache which is default)
# The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh
CACHE_MAX_TIME_CHANGING = 24
# (WHAT: Chapter identification string)
# used to identify a starting chapter in text mode
CHAPTER_FR = Chapitre
-# (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
-# (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
# (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
--- /dev/null
+# UI configuration options
+#
+
+
+# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
+# The directory where to store temporary files, defaults to a directory 'fanfic-reader' in the system default temporary directory
+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
+# (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
+# (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
import be.nikiroo.fanfix.Instance;
import be.nikiroo.fanfix.Library;
-import be.nikiroo.fanfix.bundles.Config;
-import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.bundles.UiConfig;
import be.nikiroo.fanfix.data.Story;
import be.nikiroo.fanfix.output.BasicOutput.OutputType;
}
// TODO: can throw an exception, manage that (convert to IOEx ?)
- OutputType text = OutputType.valueOfNullOkUC(Instance.getConfig()
- .getString(Config.LOCAL_READER_NON_IMAGES_DOCUMENT_TYPE));
+ OutputType text = OutputType.valueOfNullOkUC(Instance.getUiConfig()
+ .getString(UiConfig.LOCAL_READER_NON_IMAGES_DOCUMENT_TYPE));
if (text == null) {
text = OutputType.HTML;
}
- OutputType images = OutputType.valueOfNullOkUC(Instance.getConfig()
- .getString(Config.LOCAL_READER_IMAGES_DOCUMENT_TYPE));
+ OutputType images = OutputType.valueOfNullOkUC(Instance.getUiConfig()
+ .getString(UiConfig.LOCAL_READER_IMAGES_DOCUMENT_TYPE));
if (images == null) {
images = OutputType.CBZ;
}
public void read(int chapter) {
}
- // return new luid
- public String imprt(String luid) throws IOException {
+ // keep same luid
+ public void imprt(String luid) throws IOException {
try {
Story story = Instance.getLibrary().getStory(luid);
if (story != null) {
- story = lib.save(story);
- return story.getMeta().getLuid();
+ story = lib.save(story, luid);
} else {
throw new IOException("Cannot find story in Library: " + luid);
}
}
public File getTarget(String luid) throws IOException {
- MetaData meta = lib.getInfo(luid);
File file = lib.getFile(luid);
if (file == null) {
- luid = imprt(luid);
+ imprt(luid);
file = lib.getFile(luid);
- meta = lib.getInfo(luid);
}
return file;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
+import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
-import javax.swing.JTextArea;
import be.nikiroo.fanfix.data.MetaData;
public void action(LocalReaderBook book);
}
+ private static final int COVER_WIDTH = 100;
+ private static final int COVER_HEIGHT = 150;
+ private static final int SPINE_WIDTH = 5;
+ private static final int SPINE_HEIGHT = 5;
+ private static final int HOFFSET = 20;
+ private static final Color SPINE_COLOR_BOTTOM = new Color(180, 180, 180);
+ private static final Color SPINE_COLOR_RIGHT = new Color(100, 100, 100);
+ private static final int TEXT_WIDTH = COVER_WIDTH + 40;
+ private static final int TEXT_HEIGHT = 50;
+ private static final String AUTHOR_COLOR = "#888888";
+
private static final long serialVersionUID = 1L;
private JLabel icon;
- private JTextArea title;
- private JTextArea author;
+ private JLabel tt;
private boolean selected;
private boolean hovered;
private Date lastClick;
public LocalReaderBook(MetaData meta) {
if (meta.getCover() != null) {
- BufferedImage resizedImage = new BufferedImage(100, 150,
+ BufferedImage resizedImage = new BufferedImage(SPINE_WIDTH
+ + COVER_WIDTH, SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g = resizedImage.createGraphics();
- g.drawImage(meta.getCover(), 0, 0, 100, 150, null);
+ g.setColor(Color.white);
+ g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
+ g.drawImage(meta.getCover(), 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT,
+ null);
g.dispose();
icon = new JLabel(new ImageIcon(resizedImage));
} else {
+ // TODO: a big black "X" ?
icon = new JLabel(" [ no cover ] ");
}
- title = new JTextArea(meta.getTitle());
- title.setWrapStyleWord(true);
- title.setLineWrap(true);
- title.setEditable(false);
- title.setBackground(new Color(0, true));
- author = new JTextArea("by " + meta.getAuthor());
-
- this.setLayout(new BorderLayout());
+ String optAuthor = meta.getAuthor();
+ if (optAuthor != null && !optAuthor.isEmpty()) {
+ optAuthor = "(" + optAuthor + ")";
+ }
+ tt = new JLabel(
+ String.format(
+ "<html>"
+ + "<body style='width: %d px; height: %d px; text-align: center'>"
+ + "%s" + "<br>" + "<span style='color: %s;'>"
+ + "%s" + "</span>" + "</body>" + "</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);
+ this.add(tt, BorderLayout.SOUTH);
setupListeners();
setSelected(false);
public void paint(Graphics g) {
super.paint(g);
+ int h = COVER_HEIGHT;
+ int w = COVER_WIDTH;
+ int xOffset = (TEXT_WIDTH - COVER_WIDTH) - 4;
+
+ int[] xs = new int[] { xOffset, xOffset + SPINE_WIDTH,
+ xOffset + w + SPINE_WIDTH, xOffset + w };
+ int[] ys = new int[] { HOFFSET + h, HOFFSET + h + SPINE_HEIGHT,
+ HOFFSET + h + SPINE_HEIGHT, HOFFSET + h };
+ g.setColor(SPINE_COLOR_BOTTOM);
+ g.fillPolygon(new Polygon(xs, ys, xs.length));
+ xs = new int[] { xOffset + w, xOffset + w + SPINE_WIDTH,
+ xOffset + w + SPINE_WIDTH, xOffset + w };
+ ys = new int[] { HOFFSET, HOFFSET + SPINE_HEIGHT,
+ HOFFSET + h + SPINE_HEIGHT, HOFFSET + h };
+ g.setColor(SPINE_COLOR_RIGHT);
+ g.fillPolygon(new Polygon(xs, ys, xs.length));
+
Color color = new Color(255, 255, 255, 0);
if (selected && !hovered) {
color = new Color(80, 80, 100, 40);
package be.nikiroo.fanfix.reader;
import java.awt.BorderLayout;
+import java.awt.Color;
import java.awt.Desktop;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import be.nikiroo.fanfix.Instance;
import be.nikiroo.fanfix.Main;
+import be.nikiroo.fanfix.bundles.UiConfig;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListner;
+import be.nikiroo.utils.WrapLayout;
class LocalReaderFrame extends JFrame {
private static final long serialVersionUID = 1L;
private List<LocalReaderBook> books;
private JPanel bookPane;
private String type;
+ private Color color;
public LocalReaderFrame(LocalReader reader, String type) {
- super("HTML reader");
+ super("Fanfix Library");
this.reader = reader;
setLayout(new BorderLayout());
books = new ArrayList<LocalReaderBook>();
- bookPane = new JPanel(new WrapLayout(WrapLayout.LEADING));
+ bookPane = new JPanel(new WrapLayout(WrapLayout.LEADING, 5, 5));
+
+ color = null;
+ String bg = Instance.getUiConfig().getString(UiConfig.BACKGROUND_COLOR);
+ if (bg.startsWith("#") && bg.length() == 7) {
+ try {
+ color = new Color(Integer.parseInt(bg.substring(1, 3), 16),
+ Integer.parseInt(bg.substring(3, 5), 16),
+ Integer.parseInt(bg.substring(5, 7), 16));
+ } catch (NumberFormatException e) {
+ color = null; // no changes
+ e.printStackTrace();
+ }
+ }
+
+ if (color != null) {
+ setBackground(color);
+ bookPane.setBackground(color);
+ }
- add(new JScrollPane(bookPane), BorderLayout.CENTER);
+ JScrollPane scroll = new JScrollPane(bookPane);
+ scroll.getVerticalScrollBar().setUnitIncrement(16);
+ add(scroll, BorderLayout.CENTER);
refreshBooks(type);
setJMenuBar(createMenu());
bookPane.removeAll();
for (MetaData meta : stories) {
LocalReaderBook book = new LocalReaderBook(meta);
+ if (color != null) {
+ book.setBackground(color);
+ }
+
books.add(book);
final String luid = meta.getLuid();
book.addActionListener(new BookActionListner() {
imprt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String url = JOptionPane.showInputDialog(LocalReaderFrame.this,
- "url?");
- if (Main.imprt(url) != 0) {
- JOptionPane.showMessageDialog(LocalReaderFrame.this,
- "Cannot import", "Imort error",
- JOptionPane.ERROR_MESSAGE);
- } else {
- refreshBooks(type);
+ "url of the story to import?\n" + "\n"
+ + "Note: it will currently make the UI \n"
+ + "unresponsive until it is downloaded...",
+ "Importing from URL", JOptionPane.QUESTION_MESSAGE);
+ if (url != null && !url.isEmpty()) {
+ if (Main.imprt(url) != 0) {
+ JOptionPane.showMessageDialog(LocalReaderFrame.this,
+ "Cannot import: " + url, "Imort error",
+ JOptionPane.ERROR_MESSAGE);
+ } else {
+ refreshBooks(type);
+ }
}
}
});
+++ /dev/null
-package be.nikiroo.fanfix.reader;
-
-import java.awt.Component;
-import java.awt.Container;
-import java.awt.Dimension;
-import java.awt.FlowLayout;
-import java.awt.Insets;
-
-import javax.swing.JScrollPane;
-import javax.swing.SwingUtilities;
-
-/**
- * FlowLayout subclass that fully supports wrapping of components.
- *
- * @author https://tips4java.wordpress.com/2008/11/06/wrap-layout/
- */
-class WrapLayout extends FlowLayout {
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new <code>WrapLayout</code> with a left alignment and a
- * default 5-unit horizontal and vertical gap.
- */
- public WrapLayout() {
- super();
- }
-
- /**
- * Constructs a new <code>FlowLayout</code> with the specified alignment and
- * a default 5-unit horizontal and vertical gap. The value of the alignment
- * argument must be one of <code>WrapLayout</code>, <code>WrapLayout</code>,
- * or <code>WrapLayout</code>.
- *
- * @param align
- * the alignment value
- */
- public WrapLayout(int align) {
- super(align);
- }
-
- /**
- * Creates a new flow layout manager with the indicated alignment and the
- * indicated horizontal and vertical gaps.
- * <p>
- * The value of the alignment argument must be one of
- * <code>WrapLayout</code>, <code>WrapLayout</code>, or
- * <code>WrapLayout</code>.
- *
- * @param align
- * the alignment value
- * @param hgap
- * the horizontal gap between components
- * @param vgap
- * the vertical gap between components
- */
- public WrapLayout(int align, int hgap, int vgap) {
- super(align, hgap, vgap);
- }
-
- /**
- * Returns the preferred dimensions for this layout given the <i>visible</i>
- * components in the specified target container.
- *
- * @param target
- * the component which needs to be laid out
- * @return the preferred dimensions to lay out the subcomponents of the
- * specified container
- */
- @Override
- public Dimension preferredLayoutSize(Container target) {
- return layoutSize(target, true);
- }
-
- /**
- * Returns the minimum dimensions needed to layout the <i>visible</i>
- * components contained in the specified target container.
- *
- * @param target
- * the component which needs to be laid out
- * @return the minimum dimensions to lay out the subcomponents of the
- * specified container
- */
- @Override
- public Dimension minimumLayoutSize(Container target) {
- Dimension minimum = layoutSize(target, false);
- minimum.width -= (getHgap() + 1);
- return minimum;
- }
-
- /**
- * Returns the minimum or preferred dimension needed to layout the target
- * container.
- *
- * @param target
- * target to get layout size for
- * @param preferred
- * should preferred size be calculated
- * @return the dimension to layout the target container
- */
- private Dimension layoutSize(Container target, boolean preferred) {
- synchronized (target.getTreeLock()) {
- // Each row must fit with the width allocated to the containter.
- // When the container width = 0, the preferred width of the
- // container
- // has not yet been calculated so lets ask for the maximum.
-
- int targetWidth = target.getSize().width;
- Container container = target;
-
- while (container.getSize().width == 0
- && container.getParent() != null) {
- container = container.getParent();
- }
-
- targetWidth = container.getSize().width;
-
- if (targetWidth == 0)
- targetWidth = Integer.MAX_VALUE;
-
- int hgap = getHgap();
- int vgap = getVgap();
- Insets insets = target.getInsets();
- int horizontalInsetsAndGap = insets.left + insets.right
- + (hgap * 2);
- int maxWidth = targetWidth - horizontalInsetsAndGap;
-
- // Fit components into the allowed width
-
- Dimension dim = new Dimension(0, 0);
- int rowWidth = 0;
- int rowHeight = 0;
-
- int nmembers = target.getComponentCount();
-
- for (int i = 0; i < nmembers; i++) {
- Component m = target.getComponent(i);
-
- if (m.isVisible()) {
- Dimension d = preferred ? m.getPreferredSize() : m
- .getMinimumSize();
-
- // Can't add the component to current row. Start a new row.
-
- if (rowWidth + d.width > maxWidth) {
- addRow(dim, rowWidth, rowHeight);
- rowWidth = 0;
- rowHeight = 0;
- }
-
- // Add a horizontal gap for all components after the first
-
- if (rowWidth != 0) {
- rowWidth += hgap;
- }
-
- rowWidth += d.width;
- rowHeight = Math.max(rowHeight, d.height);
- }
- }
-
- addRow(dim, rowWidth, rowHeight);
-
- dim.width += horizontalInsetsAndGap;
- dim.height += insets.top + insets.bottom + vgap * 2;
-
- // When using a scroll pane or the DecoratedLookAndFeel we need to
- // make sure the preferred size is less than the size of the
- // target containter so shrinking the container size works
- // correctly. Removing the horizontal gap is an easy way to do this.
-
- Container scrollPane = SwingUtilities.getAncestorOfClass(
- JScrollPane.class, target);
-
- if (scrollPane != null && target.isValid()) {
- dim.width -= (hgap + 1);
- }
-
- return dim;
- }
- }
-
- /*
- * A new row has been completed. Use the dimensions of this row to update
- * the preferred size for the container.
- *
- * @param dim update the width and height when appropriate
- *
- * @param rowWidth the width of the row to add
- *
- * @param rowHeight the height of the row to add
- */
- private void addRow(Dimension dim, int rowWidth, int rowHeight) {
- dim.width = Math.max(dim.width, rowWidth);
-
- if (dim.height > 0) {
- dim.height += getVgap();
- }
-
- dim.height += rowHeight;
- }
-}
boolean singleQ = line.startsWith("" + openQuote);
boolean doubleQ = line.startsWith("" + openDoubleQuote);
+ // Do not try when more than one quote at a time
+ // (some stories are not easily readable if we do)
+ if (singleQ
+ && line.indexOf(closeQuote, 1) < line
+ .lastIndexOf(closeQuote)) {
+ newParas.add(para);
+ return newParas;
+ }
+ if (doubleQ
+ && line.indexOf(closeDoubleQuote, 1) < line
+ .lastIndexOf(closeDoubleQuote)) {
+ newParas.add(para);
+ return newParas;
+ }
+ //
+
if (!singleQ && !doubleQ) {
line = openDoubleQuote + line + closeDoubleQuote;
newParas.add(new Paragraph(ParagraphType.QUOTE, line));
}
private String getAuthor(InputStream in) {
+ String author = null;
+
int i = 0;
@SuppressWarnings("resource")
Scanner scan = new Scanner(in, "UTF-8");
String line = scan.next();
if (line.contains("xcontrast_txt")) {
if ((++i) == 3) {
- return StringUtils.unhtml(line).trim();
+ author = StringUtils.unhtml(line).trim();
+ break;
}
}
}
- return null;
+ return fixAuthor(author);
}
private String getDate(InputStream in) {