(If you are interested in the recent changes, please check the [Changelog](changelog.md) -- note that starting from version 1.4.0, the changelog is checked at startup (unless the option is disabled))
+TODO: new screenshots + TUI screenshots
+
![Main GUI](screenshots/fanfix-1.3.2.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, LaTeX, HTML...).
- ```--read [id] ([chapter number])```: read the given story denoted by ID from the library
- ```--read-url [URL] ([chapter number])```: convert on the fly and read the story denoted by ID, without saving it
- ```--list```: list the stories present in the library and their associated IDs
-- ```--set-reader [reader type]```: set the reader type to CLI or LOCAL for this command (must be the first option)
+- ```--set-reader [reader type]```: set the reader type to CLI, TUI or GUI for this command
+- ```--server [port]```: start a story server on this port
+- ```--remote [host] [port]```: contact this server instead of the usual library
- ```--help```: display the available options
### Environment variables
- libs/nikiroo-utils-sources.jar: some shared utility functions I also use elsewhere
- [libs/unbescape-sources.jar](https://github.com/unbescape/unbescape): a nice library to escape/unescape a lot of text formats; I only use it for HTML
+- [libs/jexer-sources.jar](https://github.com/klamonte/jexer): a small library that offers TUI widgets
Nothing else but Java 1.6+.
- [x] Implement it from --set-reader to the actual window
- [x] List the stories
- [ ] Fix the UI layout
- - [ ] Status bar and real menus
- - [ ] Open a story in the reader and/or natively
+ - [x] Status bar
+ - [ ] Real menus
+ - [x] Open a story in the reader and/or natively
+ - [ ] Update the screenshots
+- [ ] Network support
+ - [x] A server that can send the stories
+ - [x] A network implementation of the Library
+ - [ ] Write access to the library (?)
+ - [ ] Access rights (?)
+ - [ ] More tests, especially with the GUI
- [ ] Check if it can work on Android
- [x] First checks: it should work, but with changes
- [ ] Adapt work on images :(
## Version WIP
- New reader type: TUI (a text user interface with windows and menus)
+- A server option to offer stories on the network
+- A remote library to get said stories from the network
## Version 1.5.3
- FimFiction: Fix tags and chapter handling for some stories
private static boolean debug;
private static File coverDir;
private static File readerTmp;
+ private static File remoteDir;
private static String configDir;
static {
coverDir = getFile(Config.DEFAULT_COVERS_DIR);
File tmp = getFile(Config.CACHE_DIR);
readerTmp = getFile(UiConfig.CACHE_DIR_LOCAL_READER);
+ remoteDir = new File(getFile(Config.LIBRARY_DIR), "remote");
if (checkEnv("NOUTF")) {
trans.setUnicode(false);
return readerTmp;
}
+ /**
+ * Return the directory where to store temporary files for the remote
+ * {@link Library}.
+ *
+ * @param host
+ * the remote for this host
+ *
+ * @return the directory
+ */
+ public static File getRemoteDir(String host) {
+ remoteDir.mkdirs();
+
+ if (host != null) {
+ return new File(remoteDir, host);
+ }
+
+ return remoteDir;
+ }
+
/**
* Check if we need to check that a new version of Fanfix is available.
*
public class Library {
protected File baseDir;
protected boolean localSpeed;
+ protected Map<MetaData, File> stories;
- private Map<MetaData, File> stories;
private int lastId;
private OutputType text;
private OutputType image;
*
* @return the stories
*/
- public synchronized List<MetaData> getListByType(String type) {
- if (type != null) {
- // convert the type to dir name
- type = getExpectedDir(type).getName();
- }
-
+ public synchronized List<MetaData> getListBySource(String type) {
List<MetaData> list = new ArrayList<MetaData>();
- for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
- String storyType = entry.getValue().getParentFile().getName();
+ for (MetaData meta : getStories(null).keySet()) {
+ String storyType = meta.getSource();
if (type == null || type.equalsIgnoreCase(storyType)) {
- list.add(entry.getKey());
+ list.add(meta);
}
}
import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
import be.nikiroo.utils.Progress;
import be.nikiroo.utils.Version;
-import be.nikiroo.utils.ui.UIUtils;
+import be.nikiroo.utils.serial.Server;
/**
* Main program entry point.
*/
public class Main {
private enum MainAction {
- IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION,
+ IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION, SERVER, REMOTE,
}
/**
* <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
* for this command</li>
* <li>--version: get the version of the program</li>
+ * <li>--server [port]: start a server on this port</li>
+ * <li>--remote [host] [port]: use a the given remote library</li>
* </ul>
*
* @param args
public static void main(String[] args) {
String urlString = null;
String luid = null;
- String typeString = null;
+ String sourceString = null;
String chapString = null;
String target = null;
MainAction action = MainAction.START;
Boolean plusInfo = null;
+ String host = null;
+ Integer port = null;
boolean noMoreActions = false;
case EXPORT:
if (luid == null) {
luid = args[i];
- } else if (typeString == null) {
- typeString = args[i];
+ } else if (sourceString == null) {
+ sourceString = args[i];
} else if (target == null) {
target = args[i];
} else {
case CONVERT:
if (urlString == null) {
urlString = args[i];
- } else if (typeString == null) {
- typeString = args[i];
+ } else if (sourceString == null) {
+ sourceString = args[i];
} else if (target == null) {
target = args[i];
} else if (plusInfo == null) {
}
break;
case LIST:
- if (typeString == null) {
- typeString = args[i];
+ if (sourceString == null) {
+ sourceString = args[i];
} else {
exitCode = 255;
}
break;
case VERSION:
exitCode = 255; // no arguments for this option
+ break;
+ case SERVER:
+ if (port == null) {
+ port = Integer.parseInt(args[i]);
+ } else {
+ exitCode = 255;
+ }
+ break;
+ case REMOTE:
+ if (host == null) {
+ host = args[i];
+ } else if (port == null) {
+ port = Integer.parseInt(args[i]);
+ try {
+ BasicReader.setDefaultLibrary(new RemoteLibrary(host,
+ port));
+ } catch (IOException e) {
+ Instance.syserr(e);
+ }
+ action = MainAction.START;
+ } else {
+ exitCode = 255;
+ }
+ break;
}
}
updates.ok(); // we consider it read
break;
case EXPORT:
- exitCode = export(luid, typeString, target, pg);
+ exitCode = export(luid, sourceString, target, pg);
updates.ok(); // we consider it read
break;
case CONVERT:
- exitCode = convert(urlString, typeString, target,
+ exitCode = convert(urlString, sourceString, target,
plusInfo == null ? false : plusInfo, pg);
updates.ok(); // we consider it read
break;
case LIST:
- exitCode = list(typeString);
+ exitCode = list(sourceString);
break;
case READ:
exitCode = read(luid, chapString, true);
exitCode = 0;
break;
case SET_READER:
+ exitCode = 255;
break;
case VERSION:
System.out
updates.ok(); // we consider it read
break;
case START:
- //BasicReader.setDefaultReaderType(ReaderType.LOCAL);
- BasicReader.getReader().start(null);
+ BasicReader.getReader().browse(null);
+ break;
+ case SERVER:
+ if (port == null) {
+ exitCode = 255;
+ break;
+ }
+ try {
+ Server server = new RemoteLibraryServer(port);
+ server.start();
+ System.out.println("Remote server started on: " + port);
+ } catch (IOException e) {
+ Instance.syserr(e);
+ }
+ return;
+ case REMOTE:
+ exitCode = 255;
break;
}
}
}
/**
- * List the stories of the given type from the {@link Library} (unless NULL
- * is passed, in which case all stories will be listed).
+ * List the stories of the given source from the {@link Library} (unless
+ * NULL is passed, in which case all stories will be listed).
*
- * @param type
- * the type to list the known stories of, or NULL to list all
+ * @param source
+ * the source to list the known stories of, or NULL to list all
* stories
*
* @return the exit return code (0 = success)
*/
- private static int list(String type) {
- BasicReader.getReader().start(type);
+ private static int list(String source) {
+ BasicReader.getReader().browse(source);
return 0;
}
--- /dev/null
+package be.nikiroo.fanfix;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.fanfix.output.BasicOutput.OutputType;
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.ConnectActionClient;
+
+public class RemoteLibrary extends Library {
+ private String host;
+ private int port;
+
+ private Library lib;
+
+ public RemoteLibrary(String host, int port) throws IOException {
+ this.host = host;
+ this.port = port;
+
+ this.localSpeed = false;
+ this.baseDir = Instance.getRemoteDir(host);
+ this.baseDir.mkdirs();
+
+ this.lib = new Library(baseDir, OutputType.INFO_TEXT, OutputType.CBZ);
+ }
+
+ @Override
+ public synchronized Story save(Story story, String luid, Progress pg)
+ throws IOException {
+ throw new java.lang.InternalError(
+ "No write support allowed on remote Libraries");
+ }
+
+ @Override
+ public synchronized boolean delete(String luid) {
+ throw new java.lang.InternalError(
+ "No write support allowed on remote Libraries");
+ }
+
+ @Override
+ public synchronized boolean changeType(String luid, String newType) {
+ throw new java.lang.InternalError(
+ "No write support allowed on remote Libraries");
+ }
+
+ @Override
+ protected synchronized Map<MetaData, File> getStories(Progress pg) {
+ // TODO: progress
+ if (stories.isEmpty()) {
+ try {
+ new ConnectActionClient(host, port, true, null) {
+ public void action(Version serverVersion) throws Exception {
+ try {
+ Object rep = send("GET_METADATA *");
+ for (MetaData meta : (MetaData[]) rep) {
+ stories.put(meta, null);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }.connect();
+ } catch (IOException e) {
+ Instance.syserr(e);
+ }
+ }
+
+ return stories;
+ }
+
+ @Override
+ public synchronized File getFile(final String luid) {
+ File file = lib.getFile(luid);
+ if (file == null) {
+ final File[] tmp = new File[1];
+ try {
+ new ConnectActionClient(host, port, true, null) {
+ public void action(Version serverVersion) throws Exception {
+ try {
+ Object rep = send("GET_STORY " + luid);
+ Story story = (Story) rep;
+ if (story != null) {
+ lib.save(story, luid, null);
+ tmp[0] = lib.getFile(luid);
+ }
+ } catch (Exception e) {
+ Instance.syserr(e);
+ }
+ }
+ }.connect();
+ } catch (IOException e) {
+ Instance.syserr(e);
+ }
+
+ file = tmp[0];
+ }
+
+ if (file != null) {
+ MetaData meta = getInfo(luid);
+ stories.put(meta, file);
+ }
+
+ return file;
+ }
+}
--- /dev/null
+package be.nikiroo.fanfix;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.ConnectActionServer;
+import be.nikiroo.utils.serial.Server;
+
+public class RemoteLibraryServer extends Server {
+
+ public RemoteLibraryServer(int port) throws IOException {
+ super(Version.getCurrentVersion(), port, true);
+ }
+
+ @Override
+ protected Object onRequest(ConnectActionServer action,
+ Version clientVersion, Object data) throws Exception {
+ String command = null;
+ String args = null;
+ if (data instanceof String) {
+ command = (String) data;
+ int pos = command.indexOf(" ");
+ if (pos >= 0) {
+ args = command.substring(pos + 1);
+ command = command.substring(0, pos);
+ }
+ }
+
+ System.out.println(String.format("COMMAND: [%s], ARGS: [%s]", command,
+ args));
+
+ if (command != null) {
+ if (command.equals("GET_METADATA")) {
+ if (args != null && args.equals("*")) {
+ Map<MetaData, File> stories = Instance.getLibrary()
+ .getStories(null);
+ return stories.keySet().toArray(new MetaData[] {});
+ }
+ } else if (command.equals("GET_STORY")) {
+ if (args != null) {
+ return Instance.getLibrary().getStory(args, null);
+ }
+ }
+ }
+
+ return null;
+ }
+}
private List<Paragraph> empty = new ArrayList<Paragraph>();
private long words;
+ /**
+ * Empty constructor, not to use.
+ */
+ private Chapter() {
+ // for serialisation purposes
+ }
+
/**
* Create a new {@link Chapter} with the given information.
*
private String content;
private long words;
+ /**
+ * Empty constructor, not to use.
+ */
+ private Paragraph() {
+ // for serialisation purposes
+ }
+
/**
* Create a new {@link Paragraph} with the given image.
*
import be.nikiroo.fanfix.data.Story;
import be.nikiroo.fanfix.supported.BasicSupport;
import be.nikiroo.utils.Progress;
-import be.nikiroo.utils.ui.UIUtils;
import be.nikiroo.utils.serial.SerialUtils;
/**
GUI,
/** A text (UTF-8) reader with menu and text windows */
TUI,
-
+
;
-
+
public String getTypeName() {
String pkg = "be.nikiroo.fanfix.reader.";
switch (this) {
- case CLI: return pkg + "CliReader";
- case TUI: return pkg + "TuiReader";
- case GUI: return pkg + "LocalReader";
+ case CLI:
+ return pkg + "CliReader";
+ case TUI:
+ return pkg + "TuiReader";
+ case GUI:
+ return pkg + "LocalReader";
}
-
+
return null;
}
}
+ private static Library defaultLibrary = Instance.getLibrary();
private static ReaderType defaultType = ReaderType.GUI;
+
+ private Library lib;
private Story story;
private ReaderType type;
return story;
}
+ /**
+ * The {@link Library} to load the stories from (by default, takes the
+ * default {@link Library}).
+ *
+ * @return the {@link Library}
+ */
+ public Library getLibrary() {
+ if (lib == null) {
+ lib = defaultLibrary;
+ }
+
+ return lib;
+ }
+
+ /**
+ * Change the {@link Library} that will be managed by this
+ * {@link BasicReader}.
+ *
+ * @param lib
+ * the new {@link Library}
+ */
+ public void setLibrary(Library lib) {
+ this.lib = lib;
+ }
+
/**
* Create a new {@link BasicReader} for a {@link Story} in the
- * {@link Library} .
+ * {@link Library}.
*
* @param luid
* the {@link Story} ID
* in case of I/O error
*/
public void setStory(String luid, Progress pg) throws IOException {
- story = Instance.getLibrary().getStory(luid, pg);
+ story = lib.getStory(luid, pg);
if (story == null) {
throw new IOException("Cannot retrieve story from library: " + luid);
}
public abstract void read(int chapter) throws IOException;
/**
- * Start the reader in browse mode for the given type (or pass NULL for all
- * types).
+ * Start the reader in browse mode for the given source (or pass NULL for
+ * all sources).
*
- * @param type
+ * @param library
+ * the library to browse
+ *
+ * @param source
* the type of {@link Story} to take into account, or NULL for
* all
*/
- public abstract void start(String type);
+ public abstract void browse(String source);
/**
* Return a new {@link BasicReader} ready for use if one is configured.
public static BasicReader getReader() {
try {
if (defaultType != null) {
- return ((BasicReader)SerialUtils.createObject
- (defaultType.getTypeName())).setType(defaultType);
+ return ((BasicReader) SerialUtils.createObject(defaultType
+ .getTypeName())).setType(defaultType);
}
} catch (Exception e) {
Instance.syserr(new Exception("Cannot create a reader of type: "
BasicReader.defaultType = defaultType;
}
+ /**
+ * Change the default {@link Library} to open with the {@link BasicReader}s.
+ *
+ * @param lib
+ * the new {@link Library}
+ */
+ public static void setDefaultLibrary(Library lib) {
+ BasicReader.defaultLibrary = lib;
+ }
+
/**
* Return an {@link URL} from this {@link String}, be it a file path or an
* actual {@link URL}.
}
// open with external player the related file
- public static void open(String luid) throws IOException {
- MetaData meta = Instance.getLibrary().getInfo(luid);
- File target = Instance.getLibrary().getFile(luid);
+ public static void open(Library lib, String luid) throws IOException {
+ MetaData meta = lib.getInfo(luid);
+ File target = lib.getFile(luid);
open(meta, target);
}
}
@Override
- public void start(String type) {
+ public void browse(String source) {
List<MetaData> stories;
- stories = Instance.getLibrary().getListByType(type);
+ stories = getLibrary().getListBySource(source);
for (MetaData story : stories) {
String author = "";
import be.nikiroo.fanfix.output.BasicOutput.OutputType;
import be.nikiroo.utils.Progress;
import be.nikiroo.utils.Version;
+import be.nikiroo.utils.ui.UIUtils;
class LocalReader extends BasicReader {
- private Library lib;
+ static private boolean nativeLookLoaded;
+
+ private Library localLibrary;
public LocalReader() throws IOException {
+ if (!nativeLookLoaded) {
+ UIUtils.setLookAndFeel();
+ nativeLookLoaded = true;
+ }
+
File dir = Instance.getReaderDir();
dir.mkdirs();
if (!dir.exists()) {
key, value), e);
}
- lib = new Library(dir, text, images);
+ localLibrary = new Library(dir, text, images);
}
@Override
try {
Story story = Instance.getLibrary().getStory(luid, pgGetStory);
if (story != null) {
- story = lib.save(story, luid, pgSave);
+ story = localLibrary.save(story, luid, pgSave);
} else {
throw new IOException("Cannot find story in Library: " + luid);
}
* @return TRUE if it is
*/
public boolean isCached(String luid) {
- return lib.getInfo(luid) != null;
+ return localLibrary.getInfo(luid) != null;
}
@Override
- public void start(String type) {
+ public void browse(String type) {
// TODO: improve presentation of update message
final VersionCheck updates = VersionCheck.check();
StringBuilder builder = new StringBuilder();
// delete from local reader library
void clearLocalReaderCache(String luid) {
- lib.delete(luid);
+ localLibrary.delete(luid);
}
// delete from main library
void delete(String luid) {
- lib.delete(luid);
+ localLibrary.delete(luid);
Instance.getLibrary().delete(luid);
}
// open the given book
void open(String luid, Progress pg) throws IOException {
- File file = lib.getFile(luid);
+ File file = localLibrary.getFile(luid);
if (file == null) {
imprt(luid, pg);
- file = lib.getFile(luid);
+ file = localLibrary.getFile(luid);
}
- open(Instance.getLibrary().getInfo(luid), file);
+ open(getLibrary().getInfo(luid), file);
}
void changeType(String luid, String newType) {
- lib.changeType(luid, newType);
+ localLibrary.changeType(luid, newType);
Instance.getLibrary().changeType(luid, newType);
}
}
*/
private void refreshBooks() {
for (LocalReaderGroup group : booksByType.keySet()) {
- List<MetaData> stories = Instance.getLibrary().getListByType(
+ List<MetaData> stories = Instance.getLibrary().getListBySource(
booksByType.get(group));
group.refreshBooks(stories, words);
}
import java.util.List;
import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.Library;
import be.nikiroo.fanfix.data.MetaData;
class TuiReader extends BasicReader {
throw new IOException("No story to read");
}
- open(getStory().getMeta().getLuid());
+ open(getLibrary(), getStory().getMeta().getLuid());
}
@Override
}
@Override
- public void start(String type) {
- List<MetaData> stories = Instance.getLibrary().getListByType(type);
+ public void browse(String source) {
+ List<MetaData> stories = getLibrary().getListBySource(source);
try {
TuiReaderApplication app = new TuiReaderApplication(stories, this);
new Thread(app).start();
import jexer.TApplication;
import jexer.TMessageBox;
-import be.nikiroo.fanfix.Instance;
import be.nikiroo.fanfix.data.MetaData;
public class TuiReaderApplication extends TApplication {
public void open(MetaData meta) {
// TODO: open in editor + external option
- if (true) {
- if (!meta.isImageDocument()) {
- new TuiReaderStoryWindow(this, meta);
- } else {
+ if (!meta.isImageDocument()) {
+ new TuiReaderStoryWindow(this, reader.getLibrary(), meta);
+ } else {
+ try {
+ BasicReader.open(reader.getLibrary(), meta.getLuid());
+ } catch (IOException e) {
messageBox("Error when trying to open the story",
- "Images document not yet supported.",
- TMessageBox.Type.OK);
+ e.getMessage(), TMessageBox.Type.OK);
}
- return;
- }
- try {
- reader.open(meta.getLuid());
- } catch (IOException e) {
- messageBox("Error when trying to open the story", e.getMessage(),
- TMessageBox.Type.OK);
}
}
}
import jexer.TWindow;
import jexer.event.TResizeEvent;
import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.Library;
import be.nikiroo.fanfix.data.Chapter;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.data.Paragraph;
import be.nikiroo.fanfix.data.Story;
public class TuiReaderStoryWindow extends TWindow {
+ private Library lib;
private MetaData meta;
private Story story;
private TText textField;
private List<TButton> navigationButtons;
private TLabel chapterName;
- public TuiReaderStoryWindow(TApplication app, MetaData meta) {
- this(app, meta, 0);
+ public TuiReaderStoryWindow(TApplication app, Library lib, MetaData meta) {
+ this(app, lib, meta, 0);
}
- public TuiReaderStoryWindow(TApplication app, MetaData meta, int chapter) {
+ public TuiReaderStoryWindow(TApplication app, Library lib, MetaData meta,
+ int chapter) {
super(app, desc(meta), 0, 0, 60, 18, CENTERED | RESIZABLE);
+
+ this.lib = lib;
this.meta = meta;
// TODO: show all meta info before?
// -3 because 0-based and 2 for borders
int row = getHeight() - 3;
- navigationButtons.add(addButton(" ", 0, row, null)); // for bg colour when << button is pressed
+ navigationButtons.add(addButton(" ", 0, row, null)); // for bg colour
+ // when <<
+ // button is
+ // pressed
navigationButtons.add(addButton("<< ", 0, row, new TAction() {
public void DO() {
setChapter(0);
setChapter(getStory().getChapters().size());
}
}));
-
+
navigationButtons.get(0).setEnabled(false);
navigationButtons.get(1).setEnabled(false);
navigationButtons.get(2).setEnabled(false);
if (chapter != this.chapter) {
this.chapter = chapter;
-
+
int max = getStory().getChapters().size();
navigationButtons.get(0).setEnabled(chapter > 0);
navigationButtons.get(1).setEnabled(chapter > 0);
navigationButtons.get(2).setEnabled(chapter > 0);
navigationButtons.get(3).setEnabled(chapter < max);
navigationButtons.get(4).setEnabled(chapter < max);
-
+
Chapter chap;
String name;
if (chapter == 0) {
name = String.format(" %s", chap.getName());
} else {
chap = getStory().getChapters().get(chapter - 1);
- name = String.format(" %d/%d: %s", chapter, max, chap.getName());
+ name = String
+ .format(" %d/%d: %s", chapter, max, chap.getName());
}
while (name.length() < getWidth() - chapterName.getX()) {
private Story getStory() {
if (story == null) {
// TODO: progress bar?
- story = Instance.getLibrary().getStory(meta.getLuid(), null);
+ story = lib.getStory(meta.getLuid(), null);
}
return story;
}