# Fanfix
+## Version 1.2.4
+
+- new UI option: Re-download
+- fix UI: books are now sorted (will not jump around after refresh/redownload)
+- fixes on quote character handling
+- fixes on Chapter detection
+
+## Version 1.2.3
+
+- Include the original (info_text) files when saving to HTML
+- New input type supported: HTML files made by Fanfix
+
+## Version 1.2.2
+
+- New "Save as..." GUI option
+- GUI fixes (icon refresh)
+- Fix handling of TABs in user messages
+- LocalReader can now be used with --read
+- Some fixes in CSS
+
+## Version 1.2.1
+
+- Some GUI menu functions added
+- Right-click popup menu added
+- GUI fixes, especially for the LocalReader library
+- New green round icon to denote "cached" (into LocalReader library) files
+
+## Version 1.2.0
+
+- Progress reporting system in GUI, too
+- CSS style changes
+- unit tests added
+- Some GUI menu functions added (delete, refresh, a place-holder for export)
+
## Version 1.1.0
- new Progress reporting system (currently only in CLI mode)
// Most of the rest is dependent upon this:
config = new ConfigBundle();
- String configDir = System.getenv("CONFIG_DIR");
+ String configDir = System.getProperty("CONFIG_DIR");
+ if (configDir == null) {
+ configDir = System.getenv("CONFIG_DIR");
+ }
if (configDir == null) {
configDir = new File(System.getProperty("user.home"), ".fanfix")
.getPath();
}
+
if (configDir != null) {
if (!new File(configDir).exists()) {
new File(configDir).mkdirs();
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
}
}
+ Collections.sort(list);
return list;
}
*
* @author niki
*/
-public class MetaData implements Cloneable {
+public class MetaData implements Cloneable, Comparable<MetaData> {
private String title;
private String author;
private String date;
this.imageDocument = imageDocument;
}
+ public int compareTo(MetaData o) {
+ String oUuid = o == null ? null : o.getUuid();
+ return getUuid().compareTo(oUuid);
+ }
+
@Override
public MetaData clone() {
MetaData meta = null;
private Date lastClick;
private List<BookActionListener> listeners;
- private String luid;
+ private MetaData meta;
private boolean cached;
/**
*/
public LocalReaderBook(MetaData meta, boolean cached) {
this.cached = cached;
- luid = meta.getLuid();
+ this.meta = meta;
String optAuthor = meta.getAuthor();
if (optAuthor != null && !optAuthor.isEmpty()) {
}
/**
- * The Library UID of the book represented by this item.
+ * The Library {@code}link MetaData} of the book represented by this item.
*
- * @return the LUID
+ * @return the meta
*/
- public String getLuid() {
- return luid;
+ public MetaData getMeta() {
+ return meta;
}
/**
popup.add(createMenuItemOpenBook());
popup.addSeparator();
popup.add(createMenuItemExport());
- popup.add(createMenuItemRefresh());
+ popup.add(createMenuItemClearCache());
+ popup.add(createMenuItemRedownload());
popup.addSeparator();
popup.add(createMenuItemDelete());
popup.show(e.getComponent(), e.getX(), e.getY());
JMenu file = new JMenu("File");
file.setMnemonic(KeyEvent.VK_F);
- JMenuItem imprt = new JMenuItem("Import URL", KeyEvent.VK_U);
+ JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
imprt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
imprt(true);
}
});
- JMenuItem imprtF = new JMenuItem("Import File", KeyEvent.VK_F);
+ JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
imprtF.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
imprt(false);
JMenu edit = new JMenu("Edit");
edit.setMnemonic(KeyEvent.VK_E);
- edit.add(createMenuItemRefresh());
+ edit.add(createMenuItemClearCache());
+ edit.add(createMenuItemRedownload());
edit.addSeparator();
edit.add(createMenuItemDelete());
public void run() {
try {
Instance.getLibrary().export(
- selectedBook.getLuid(), type, path, pg);
+ selectedBook.getMeta().getLuid(), type,
+ path, pg);
} catch (IOException e) {
Instance.syserr(e);
}
*
* @return the item
*/
- private JMenuItem createMenuItemRefresh() {
+ private JMenuItem createMenuItemClearCache() {
JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
refresh.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (selectedBook != null) {
outOfUi(null, new Runnable() {
public void run() {
- reader.refresh(selectedBook.getLuid());
+ reader.refresh(selectedBook.getMeta().getLuid());
selectedBook.setCached(false);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
return refresh;
}
+ /**
+ * Create the redownload (then delete original) menu item.
+ *
+ * @return the item
+ */
+ private JMenuItem createMenuItemRedownload() {
+ JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
+ refresh.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (selectedBook != null) {
+ imprt(selectedBook.getMeta().getUrl(), new Runnable() {
+ public void run() {
+ reader.delete(selectedBook.getMeta().getLuid());
+ selectedBook = null;
+ }
+ });
+ }
+ }
+ });
+
+ return refresh;
+ }
+
/**
* Create the delete menu item.
*
if (selectedBook != null) {
outOfUi(null, new Runnable() {
public void run() {
- reader.delete(selectedBook.getLuid());
+ reader.delete(selectedBook.getMeta().getLuid());
selectedBook = null;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
outOfUi(pg, new Runnable() {
public void run() {
try {
- reader.open(book.getLuid(), pg);
+ reader.open(book.getMeta().getLuid(), pg);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
book.setCached(true);
/**
* Import a {@link Story} into the main {@link Library}.
+ * <p>
+ * Should be called inside the UI thread.
*
* @param askUrl
* TRUE for an {@link URL}, false for a {@link File}
private void imprt(boolean askUrl) {
JFileChooser fc = new JFileChooser();
- final String url;
+ String url;
if (askUrl) {
url = JOptionPane.showInputDialog(LocalReaderFrame.this,
"url of the story to import?", "Importing from URL",
}
if (url != null && !url.isEmpty()) {
- final Progress pg = new Progress("Importing " + url);
- outOfUi(pg, new Runnable() {
- public void run() {
- Exception ex = null;
- try {
- Instance.getLibrary()
- .imprt(BasicReader.getUrl(url), pg);
- } catch (IOException e) {
- ex = e;
- }
+ imprt(url, null);
+ }
+ }
- final Exception e = ex;
+ /**
+ * Actually import the {@link Story} into the main {@link Library}.
+ * <p>
+ * Should be called inside the UI thread.
+ *
+ * @param url
+ * the {@link Story} to import by {@link URL}
+ * @param onSuccess
+ * Action to execute on success
+ */
+ private void imprt(final String url, final Runnable onSuccess) {
+ final Progress pg = new Progress("Importing " + url);
+ outOfUi(pg, new Runnable() {
+ public void run() {
+ Exception ex = null;
+ try {
+ Instance.getLibrary().imprt(BasicReader.getUrl(url), pg);
+ } catch (IOException e) {
+ ex = e;
+ }
- final boolean ok = (e == null);
- SwingUtilities.invokeLater(new Runnable() {
- public void run() {
- if (!ok) {
- JOptionPane.showMessageDialog(
- LocalReaderFrame.this, e.getMessage(),
- "Cannot import: " + url,
- JOptionPane.ERROR_MESSAGE);
- } else {
+ final Exception e = ex;
+
+ final boolean ok = (e == null);
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (!ok) {
+ JOptionPane.showMessageDialog(
+ LocalReaderFrame.this, "Cannot import: "
+ + url, e.getMessage(),
+ JOptionPane.ERROR_MESSAGE);
+
+ setEnabled(true);
+ } else {
+ refreshBooks(type);
+ if (onSuccess != null) {
+ onSuccess.run();
refreshBooks(type);
}
}
- });
- }
- });
- }
+ }
+ });
+ }
+ });
}
/**
private InputStream in;
private SupportType type;
- private URL currentReferer; // with on 'r', as in 'HTTP'...
+ private URL currentReferer; // with only one 'r', as in 'HTTP'...
// quote chars
private char openQuote = Instance.getTrans().getChar(
*
* @return the processed {@link Paragraph}
*/
- private Paragraph processPara(String line) {
+ protected Paragraph processPara(String line) {
line = ifUnhtml(line).trim();
boolean space = true;
if (tentativeCloseQuote) {
tentativeCloseQuote = false;
- if ((car >= 'a' && car <= 'z') || (car >= 'A' && car <= 'Z')
- || (car >= '0' && car <= '9')) {
+ if (Character.isLetterOrDigit(car)) {
builder.append("'");
} else {
- builder.append(closeQuote);
+ // handle double-single quotes as double quotes
+ if (prev == car) {
+ builder.append(closeDoubleQuote);
+ continue;
+ } else {
+ builder.append(closeQuote);
+ }
}
}
case '\'':
if (space || (brk && quote)) {
quote = true;
- builder.append(openQuote);
- } else if (prev == ' ') {
- builder.append(openQuote);
+ // handle double-single quotes as double quotes
+ if (prev == car) {
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append(openDoubleQuote);
+ } else {
+ builder.append(openQuote);
+ }
+ } else if (prev == ' ' || prev == car) {
+ // handle double-single quotes as double quotes
+ if (prev == car) {
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append(openDoubleQuote);
+ } else {
+ builder.append(openQuote);
+ }
} else {
// it is a quote ("I'm off") or a 'quote' ("This
// 'good' restaurant"...)
quote = true;
builder.append(openQuote);
} else {
- builder.append(openQuote);
+ // handle double-single quotes as double quotes
+ if (prev == car) {
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append(openDoubleQuote);
+ } else {
+ builder.append(openQuote);
+ }
}
space = false;
brk = false;
case '」':
space = false;
brk = false;
- builder.append(closeQuote);
+ // handle double-single quotes as double quotes
+ if (prev == car) {
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append(closeDoubleQuote);
+ } else {
+ builder.append(closeQuote);
+ }
break;
case '«':
import java.util.Scanner;
import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.Config;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.utils.StringUtils;
if (builder.length() == 0) {
int pos = line.indexOf("<hr");
if (pos >= 0) {
- line = line.substring(pos);
+ boolean chaptered = false;
+ for (String lang : Instance.getConfig()
+ .getString(Config.CHAPTER).split(",")) {
+ String chapterWord = Instance.getConfig()
+ .getStringX(Config.CHAPTER, lang);
+ int posChap = line.indexOf(chapterWord + " ");
+ if (posChap < pos) {
+ chaptered = true;
+ break;
+ }
+ }
+
+ if (chaptered) {
+ line = line.substring(pos);
+ }
}
}
chapter0 = scan.next();
}
- String lang = detectChapter(chapter0);
+ String lang = detectChapter(chapter0, 0);
+ if (lang == null) {
+ // No description??
+ lang = detectChapter(chapter0, 1);
+ }
+
if (lang == null) {
lang = "EN";
} else {
@SuppressWarnings("resource")
Scanner scan = new Scanner(in, "UTF-8");
scan.useDelimiter("\\n");
- boolean descSkipped = false;
boolean prevLineEmpty = false;
while (scan.hasNext()) {
String line = scan.next();
- if (prevLineEmpty && detectChapter(line) != null) {
- if (descSkipped) {
- String chapName = Integer.toString(chaps.size());
- int pos = line.indexOf(':');
- if (pos >= 0 && pos + 1 < line.length()) {
- chapName = line.substring(pos + 1).trim();
+ if (prevLineEmpty && detectChapter(line, chaps.size() + 1) != null) {
+ String chapName = Integer.toString(chaps.size() + 1);
+ int pos = line.indexOf(':');
+ if (pos >= 0 && pos + 1 < line.length()) {
+ chapName = line.substring(pos + 1).trim();
+ }
+ final URL value = source;
+ final String key = chapName;
+ chaps.add(new Entry<String, URL>() {
+ public URL setValue(URL value) {
+ return null;
}
- final URL value = source;
- final String key = chapName;
- chaps.add(new Entry<String, URL>() {
- public URL setValue(URL value) {
- return null;
- }
- public URL getValue() {
- return value;
- }
+ public URL getValue() {
+ return value;
+ }
- public String getKey() {
- return key;
- }
- });
- } else {
- descSkipped = true;
- }
+ public String getKey() {
+ return key;
+ }
+ });
}
prevLineEmpty = line.trim().isEmpty();
String line = scan.next();
if (detectChapter(line, number) != null) {
inChap = true;
- } else if (inChap && detectChapter(line) != null) {
+ } else if (inChap && detectChapter(line, number + 1) != null) {
break;
} else if (inChap) {
builder.append(line);
return false;
}
- /**
- * Check if the given line looks like a starting chapter in a supported
- * language, and return the language if it does (or NULL if not).
- *
- * @param line
- * the line to check
- *
- * @return the language or NULL
- */
- private String detectChapter(String line) {
- return detectChapter(line, null);
- }
-
/**
* Check if the given line looks like the given starting chapter in a
* supported language, and return the language if it does (or NULL if not).
*
* @return the language or NULL
*/
- private String detectChapter(String line, Integer number) {
+ private String detectChapter(String line, int number) {
line = line.toUpperCase();
for (String lang : Instance.getConfig().getString(Config.CHAPTER)
.split(",")) {
if (chapter != null && !chapter.isEmpty()) {
chapter = chapter.toUpperCase() + " ";
if (line.startsWith(chapter)) {
- if (number != null) {
- // We want "[CHAPTER] [number]: [name]", with ": [name]"
- // optional
- String test = line.substring(chapter.length()).trim();
- if (test.startsWith(Integer.toString(number))) {
- test = test.substring(
- Integer.toString(number).length()).trim();
- if (test.isEmpty() || test.startsWith(":")) {
- return lang;
- }
+ // We want "[CHAPTER] [number]: [name]", with ": [name]"
+ // optional
+ String test = line.substring(chapter.length()).trim();
+ if (test.startsWith(Integer.toString(number))) {
+ test = test
+ .substring(Integer.toString(number).length())
+ .trim();
+ if (test.isEmpty() || test.startsWith(":")) {
+ return lang;
}
- } else {
- return lang;
}
}
}
package be.nikiroo.fanfix.test;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.Map.Entry;
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringId;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.data.Paragraph;
import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
+import be.nikiroo.fanfix.data.Story;
import be.nikiroo.fanfix.supported.BasicSupport;
+import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
+import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.test.TestCase;
import be.nikiroo.utils.test.TestLauncher;
public class BasicSupportTest extends TestLauncher {
+ // quote chars
+ private char openQuote = Instance.getTrans().getChar(
+ StringId.OPEN_SINGLE_QUOTE);
+ private char closeQuote = Instance.getTrans().getChar(
+ StringId.CLOSE_SINGLE_QUOTE);
+ private char openDoubleQuote = Instance.getTrans().getChar(
+ StringId.OPEN_DOUBLE_QUOTE);
+ private char closeDoubleQuote = Instance.getTrans().getChar(
+ StringId.CLOSE_DOUBLE_QUOTE);
public BasicSupportTest(String[] args) {
super("BasicSupport", args);
.getType());
}
});
+
+ addTest(new TestCase("BasicSupport.processPara() quotes") {
+ @Override
+ public void test() throws Exception {
+ BasicSupportEmpty support = new BasicSupportEmpty() {
+ @Override
+ protected boolean isHtml() {
+ return true;
+ }
+ };
+
+ Paragraph para;
+
+ // sanity check
+ para = support.processPara("");
+ assertEquals(ParagraphType.BLANK, para.getType());
+ //
+
+ para = support.processPara("\"Yes, my Lord!\"");
+ assertEquals(ParagraphType.QUOTE, para.getType());
+ assertEquals(openDoubleQuote + "Yes, my Lord!"
+ + closeDoubleQuote, para.getContent());
+
+ para = support.processPara("«Yes, my Lord!»");
+ assertEquals(ParagraphType.QUOTE, para.getType());
+ assertEquals(openDoubleQuote + "Yes, my Lord!"
+ + closeDoubleQuote, para.getContent());
+
+ para = support.processPara("'Yes, my Lord!'");
+ assertEquals(ParagraphType.QUOTE, para.getType());
+ assertEquals(openQuote + "Yes, my Lord!" + closeQuote,
+ para.getContent());
+
+ para = support.processPara("‹Yes, my Lord!›");
+ assertEquals(ParagraphType.QUOTE, para.getType());
+ assertEquals(openQuote + "Yes, my Lord!" + closeQuote,
+ para.getContent());
+ }
+ });
+
+ addTest(new TestCase(
+ "BasicSupport.processPara() double-simple quotes") {
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ };
+
+ @Override
+ public void tearDown() throws Exception {
+
+ super.tearDown();
+ }
+
+ @Override
+ public void test() throws Exception {
+ BasicSupportEmpty support = new BasicSupportEmpty() {
+ @Override
+ protected boolean isHtml() {
+ return true;
+ }
+ };
+
+ Paragraph para;
+
+ para = support.processPara("''Yes, my Lord!''");
+ assertEquals(ParagraphType.QUOTE, para.getType());
+ assertEquals(openDoubleQuote + "Yes, my Lord!"
+ + closeDoubleQuote, para.getContent());
+
+ para = support.processPara("‹‹Yes, my Lord!››");
+ assertEquals(ParagraphType.QUOTE, para.getType());
+ assertEquals(openDoubleQuote + "Yes, my Lord!"
+ + closeDoubleQuote, para.getContent());
+ }
+ });
+
+ addTest(new TestCase("BasicSupport.processPara() apostrophe") {
+ @Override
+ public void test() throws Exception {
+ BasicSupportEmpty support = new BasicSupportEmpty() {
+ @Override
+ protected boolean isHtml() {
+ return true;
+ }
+ };
+
+ Paragraph para;
+
+ String text = "Nous étions en été, mais cela aurait être l'hiver quand nous n'étions encore qu'à Aubeuge";
+ para = support.processPara(text);
+ assertEquals(ParagraphType.NORMAL, para.getType());
+ assertEquals(text, para.getContent());
+ }
+ });
+ }
+ });
+
+ addSeries(new TestLauncher("Text", args) {
+ {
+ addTest(new TestCase("Chapter detection simple") {
+ private File tmp;
+
+ @Override
+ public void setUp() throws Exception {
+ tmp = File.createTempFile("fanfix-text-file_", ".test");
+ IOUtils.writeSmallFile(tmp.getParentFile(),
+ tmp.getName(), "TITLE"
+ + "\n"//
+ + "By nona"
+ + "\n" //
+ + "\n" //
+ + "Chapter 0: Resumé" + "\n" + "\n"
+ + "'sume." + "\n" + "\n"
+ + "Chapter 1: chap1" + "\n" + "\n"
+ + "Fanfan." + "\n" + "\n"
+ + "Chapter 2: Chap2" + "\n" + "\n" //
+ + "Tulipe." + "\n");
+ };
+
+ @Override
+ public void tearDown() throws Exception {
+ tmp.delete();
+ };
+
+ @Override
+ public void test() throws Exception {
+ BasicSupport support = BasicSupport
+ .getSupport(SupportType.TEXT);
+
+ Story story = support
+ .process(tmp.toURI().toURL(), null);
+
+ assertEquals(2, story.getChapters().size());
+ assertEquals(1, story.getChapters().get(1)
+ .getParagraphs().size());
+ assertEquals("Tulipe.", story.getChapters().get(1)
+ .getParagraphs().get(0).getContent());
+ }
+ });
+
+ addTest(new TestCase("Chapter detection with String 'Chapter'") {
+ private File tmp;
+
+ @Override
+ public void setUp() throws Exception {
+ tmp = File.createTempFile("fanfix-text-file_", ".test");
+ IOUtils.writeSmallFile(tmp.getParentFile(),
+ tmp.getName(), "TITLE"
+ + "\n"//
+ + "By nona"
+ + "\n" //
+ + "\n" //
+ + "Chapter 0: Resumé" + "\n" + "\n"
+ + "'sume." + "\n" + "\n"
+ + "Chapter 1: chap1" + "\n" + "\n"
+ + "Chapter fout-la-merde" + "\n"
+ + "\n"//
+ + "Fanfan." + "\n" + "\n"
+ + "Chapter 2: Chap2" + "\n" + "\n" //
+ + "Tulipe." + "\n");
+ };
+
+ @Override
+ public void tearDown() throws Exception {
+ tmp.delete();
+ };
+
+ @Override
+ public void test() throws Exception {
+ BasicSupport support = BasicSupport
+ .getSupport(SupportType.TEXT);
+
+ Story story = support
+ .process(tmp.toURI().toURL(), null);
+
+ assertEquals(2, story.getChapters().size());
+ assertEquals(1, story.getChapters().get(1)
+ .getParagraphs().size());
+ assertEquals("Tulipe.", story.getChapters().get(1)
+ .getParagraphs().get(0).getContent());
+ }
+ });
}
});
}
public void fixBlanksBreaks(List<Paragraph> paras) {
super.fixBlanksBreaks(paras);
}
+
+ @Override
+ // and make it public!
+ public Paragraph processPara(String line) {
+ return super.processPara(line);
+ }
}
}
package be.nikiroo.fanfix.test;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+import be.nikiroo.fanfix.bundles.ConfigBundle;
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.resources.Bundles;
import be.nikiroo.utils.test.TestLauncher;
/**
*
* @param args
* the arguments passed to the {@link TestLauncher}s.
+ * @throws IOException
*/
- static public void main(String[] args) {
- System.exit(new Test(args).launch());
+ static public void main(String[] args) throws IOException {
+ File tmpConfig = File.createTempFile("fanfix-config_", ".test");
+ File tmpCache = File.createTempFile("fanfix-cache_", ".test");
+ tmpConfig.delete();
+ tmpConfig.mkdir();
+ tmpCache.delete();
+ tmpCache.mkdir();
+
+ FileOutputStream out = new FileOutputStream(new File(tmpConfig,
+ "config.properties"));
+ Properties props = new Properties();
+ props.setProperty("CACHE_DIR", tmpCache.getAbsolutePath());
+ props.store(out, null);
+ out.close();
+
+ ConfigBundle config = new ConfigBundle();
+ Bundles.setDirectory(tmpConfig.getAbsolutePath());
+ config.updateFile(tmpConfig.getPath());
+
+ System.setProperty("CONFIG_DIR", tmpConfig.getAbsolutePath());
+
+ int result = new Test(args).launch();
+
+ IOUtils.deltree(tmpConfig);
+ IOUtils.deltree(tmpCache);
+
+ System.exit(result);
}
}