From: Niki Roo Date: Tue, 27 Jun 2017 16:25:20 +0000 (+0200) Subject: Add a new TUI system based upon Jexer (WIP) X-Git-Tag: fanfix-1.6.0~39 X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=c1873e5678fabf306915c54f9c1736e03e027d60;p=fanfix.git Add a new TUI system based upon Jexer (WIP) --- diff --git a/Makefile.base b/Makefile.base index 54dbf1a..ef0b926 100644 --- a/Makefile.base +++ b/Makefile.base @@ -108,7 +108,7 @@ test-resources: resources libs: bin @[ -e bin/libs -o ! -d libs ] || echo Extracting sources from libs... - @[ -e bin/libs -o ! -d libs ] || (cd src && for lib in ../libs/*-sources.jar; do \ + @[ -e bin/libs -o ! -d libs ] || (cd src && for lib in ../libs/*-sources.jar ../libs/*-sources.patch.jar; do \ basename "$$lib"; \ jar xf "$$lib"; \ done ) diff --git a/README.md b/README.md index f66be31..826aacb 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,12 @@ Currently missing, but either in progress or planned: - [x] ..as a screen view - [x] options screen - [ ] A TUI reader + - [x] Choose an output (Jexer) + - [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 - [ ] Check if it can work on Android - [x] First checks: it should work, but with changes - [ ] Adapt work on images :( diff --git a/changelog.md b/changelog.md index f399def..89b6b00 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # Fanfix +## Version WIP +- New reader type: TUI (a text user interface with windows and menus) + ## Version 1.5.3 - FimFiction: Fix tags and chapter handling for some stories diff --git a/libs/jexer-0.0.4-sources.jar b/libs/jexer-0.0.4-sources.jar new file mode 100644 index 0000000..b0819e0 Binary files /dev/null and b/libs/jexer-0.0.4-sources.jar differ diff --git a/libs/jexer-0.0.4-sources.patch.jar b/libs/jexer-0.0.4-sources.patch.jar new file mode 100644 index 0000000..85dc502 Binary files /dev/null and b/libs/jexer-0.0.4-sources.patch.jar differ diff --git a/src/be/nikiroo/fanfix/Main.java b/src/be/nikiroo/fanfix/Main.java index a282e7c..5cd3313 100644 --- a/src/be/nikiroo/fanfix/Main.java +++ b/src/be/nikiroo/fanfix/Main.java @@ -52,8 +52,8 @@ public class Main { *
  • --read-url [URL] ([chapter number]): convert on the fly and read the * story, without saving it
  • *
  • --list ([type]): list the stories present in the library
  • - *
  • --set-reader [reader type]: set the reader type to CLI or LOCAL for - * this command
  • + *
  • --set-reader [reader type]: set the reader type to CLI, TUI or LOCAL + * for this command
  • *
  • --version: get the version of the program
  • * * @@ -157,6 +157,7 @@ public class Main { break; case SET_READER: exitCode = setReaderType(args[i]); + action = MainAction.START; break; case START: exitCode = 255; // not supposed to be selected by user @@ -246,8 +247,7 @@ public class Main { updates.ok(); // we consider it read break; case START: - UIUtils.setLookAndFeel(); - BasicReader.setDefaultReaderType(ReaderType.LOCAL); + //BasicReader.setDefaultReaderType(ReaderType.LOCAL); BasicReader.getReader().start(null); break; } diff --git a/src/be/nikiroo/fanfix/bundles/Config.java b/src/be/nikiroo/fanfix/bundles/Config.java index a38b9e2..63e6465 100644 --- a/src/be/nikiroo/fanfix/bundles/Config.java +++ b/src/be/nikiroo/fanfix/bundles/Config.java @@ -12,8 +12,8 @@ import be.nikiroo.utils.resources.Meta.Format; public enum Config { @Meta(description = "language (example: en-GB, fr-BE...) or nothing for default system language", format = Format.LOCALE, info = "Force the language (can be overwritten again with the env variable $LANG)") LANG, // - @Meta(description = "reader type (CLI = simple output to console, LOCAL = use local system file handler)", format = Format.FIXED_LIST, list = { - "CLI", "LOCAL" }, info = "Select the default reader to use to read stories") + @Meta(description = "reader type (CLI = simple output to console, TUI = Text User Interface with menus and windows, GUI = a GUI with locally stored files)", format = Format.FIXED_LIST, list = { + "CLI", "GUI", "TUI" }, info = "Select the default reader to use to read stories") READER_TYPE, // @Meta(description = "absolute path, $HOME variable supported, / is always accepted as dir separator", format = Format.DIRECTORY, info = "The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)") CACHE_DIR, // diff --git a/src/be/nikiroo/fanfix/bundles/config.properties b/src/be/nikiroo/fanfix/bundles/config.properties index 6151843..061b0fb 100644 --- a/src/be/nikiroo/fanfix/bundles/config.properties +++ b/src/be/nikiroo/fanfix/bundles/config.properties @@ -5,9 +5,9 @@ # language (example: en-GB, fr-BE...) or nothing for default system language # (FORMAT: LOCALE) Force the language (can be overwritten again with the env variable $LANG) LANG = -# reader type (CLI = simple output to console, LOCAL = use local system file handler) +# reader type (CLI = simple output to console, TUI = Text User Interface with menus and windows, GUI = a GUI with locally stored files) # (FORMAT: FIXED_LIST) Select the default reader to use to read stories -# ALLOWED VALUES: "CLI" "LOCAL" +# ALLOWED VALUES: "CLI" "GUI" "TUI" READER_TYPE = # absolute path, $HOME variable supported, / is always accepted as dir separator # (FORMAT: DIRECTORY) The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix) diff --git a/src/be/nikiroo/fanfix/bundles/resources.properties b/src/be/nikiroo/fanfix/bundles/resources.properties index f664eb4..a63fbd3 100644 --- a/src/be/nikiroo/fanfix/bundles/resources.properties +++ b/src/be/nikiroo/fanfix/bundles/resources.properties @@ -16,7 +16,7 @@ HELP_SYNTAX = Valid options:\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--set-reader [reader type]: set the reader type to CLI, TUI or GUI for this command\n\ \t--help: this help message\n\ \t--version: return the version of the program\n\ \n\ diff --git a/src/be/nikiroo/fanfix/reader/BasicReader.java b/src/be/nikiroo/fanfix/reader/BasicReader.java index ae1c8d4..74bd5d4 100644 --- a/src/be/nikiroo/fanfix/reader/BasicReader.java +++ b/src/be/nikiroo/fanfix/reader/BasicReader.java @@ -1,5 +1,6 @@ package be.nikiroo.fanfix.reader; +import java.awt.Desktop; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -8,9 +9,12 @@ import java.net.URL; import be.nikiroo.fanfix.Instance; import be.nikiroo.fanfix.Library; import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.UiConfig; +import be.nikiroo.fanfix.data.MetaData; import be.nikiroo.fanfix.data.Story; import be.nikiroo.fanfix.supported.BasicSupport; import be.nikiroo.utils.Progress; +import be.nikiroo.utils.ui.UIUtils; /** * The class that handles the different {@link Story} readers you can use. @@ -24,10 +28,12 @@ public abstract class BasicReader { /** Simple reader that outputs everything on the console */ CLI, /** Reader that starts local programs to handle the stories */ - LOCAL + GUI, + /** A text (UTF-8) reader with menu and text windows */ + TUI, } - private static ReaderType defaultType = ReaderType.CLI; + private static ReaderType defaultType = ReaderType.GUI; private Story story; private ReaderType type; @@ -162,10 +168,13 @@ public abstract class BasicReader { try { if (defaultType != null) { switch (defaultType) { - case LOCAL: - return new LocalReader().setType(ReaderType.LOCAL); + case GUI: + UIUtils.setLookAndFeel(); + return new LocalReader().setType(ReaderType.GUI); case CLI: return new CliReader().setType(ReaderType.CLI); + case TUI: + return new TuiReader().setType(ReaderType.TUI); } } } catch (IOException e) { @@ -224,4 +233,41 @@ public abstract class BasicReader { return source; } + + // 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); + + open(meta, target); + } + + // open with external player the related file + protected static void open(MetaData meta, File target) throws IOException { + 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() }); + } + } } diff --git a/src/be/nikiroo/fanfix/reader/LocalReader.java b/src/be/nikiroo/fanfix/reader/LocalReader.java index a3fdcab..2b92e72 100644 --- a/src/be/nikiroo/fanfix/reader/LocalReader.java +++ b/src/be/nikiroo/fanfix/reader/LocalReader.java @@ -16,7 +16,6 @@ import be.nikiroo.fanfix.Instance; import be.nikiroo.fanfix.Library; import be.nikiroo.fanfix.VersionCheck; 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; @@ -111,29 +110,6 @@ class LocalReader extends BasicReader { } } - /** - * Get the target file related to this {@link Story}. - * - * @param luid - * the LUID of the {@link Story} - * @param pg - * the optional progress reporter - * - * @return the target file - * - * @throws IOException - * in case of I/O error - */ - public File getTarget(String luid, Progress pg) throws IOException { - File file = lib.getFile(luid); - if (file == null) { - imprt(luid, pg); - file = lib.getFile(luid); - } - - return file; - } - /** * Check if the {@link Story} denoted by this Library UID is present in the * {@link LocalReader} cache. @@ -224,35 +200,13 @@ class LocalReader extends BasicReader { // 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; + File file = lib.getFile(luid); + if (file == null) { + imprt(luid, pg); + file = lib.getFile(luid); } - 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() }); - - } + open(Instance.getLibrary().getInfo(luid), file); } void changeType(String luid, String newType) { diff --git a/src/be/nikiroo/fanfix/reader/TuiReader.java b/src/be/nikiroo/fanfix/reader/TuiReader.java new file mode 100644 index 0000000..d18436f --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/TuiReader.java @@ -0,0 +1,35 @@ +package be.nikiroo.fanfix.reader; + +import java.io.IOException; +import java.util.List; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.MetaData; + +class TuiReader extends BasicReader { + @Override + public void read() throws IOException { + if (getStory() == null) { + throw new IOException("No story to read"); + } + + open(getStory().getMeta().getLuid()); + } + + @Override + public void read(int chapter) throws IOException { + // TODO: show a special page? + read(); + } + + @Override + public void start(String type) { + List stories = Instance.getLibrary().getListByType(type); + try { + TuiReaderApplication app = new TuiReaderApplication(stories, this); + new Thread(app).start(); + } catch (Exception e) { + Instance.syserr(e); + } + } +} diff --git a/src/be/nikiroo/fanfix/reader/TuiReaderApplication.java b/src/be/nikiroo/fanfix/reader/TuiReaderApplication.java new file mode 100644 index 0000000..3e9d1d7 --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/TuiReaderApplication.java @@ -0,0 +1,75 @@ +package be.nikiroo.fanfix.reader; + +import java.io.IOException; +import java.util.List; + +import jexer.TApplication; +import jexer.TMessageBox; +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.MetaData; + +public class TuiReaderApplication extends TApplication { + private BasicReader reader; + + private static BackendType guessBackendType() { + // Swing is the default backend on Windows unless explicitly + // overridden by jexer.Swing. + TApplication.BackendType backendType = TApplication.BackendType.XTERM; + if (System.getProperty("os.name").startsWith("Windows")) { + backendType = TApplication.BackendType.SWING; + } + if (System.getProperty("os.name").startsWith("Mac")) { + backendType = TApplication.BackendType.SWING; + } + if (System.getProperty("jexer.Swing") != null) { + if (System.getProperty("jexer.Swing", "false").equals("true")) { + backendType = TApplication.BackendType.SWING; + } else { + backendType = TApplication.BackendType.XTERM; + } + } + return backendType; + } + + public TuiReaderApplication(List stories, BasicReader reader) + throws Exception { + this(stories, reader, guessBackendType()); + } + + public TuiReaderApplication(List stories, BasicReader reader, + TApplication.BackendType backend) throws Exception { + super(backend); + + this.reader = reader; + + // Add the menus + addFileMenu(); + addEditMenu(); + addWindowMenu(); + addHelpMenu(); + + getBackend().setTitle("Testy"); + + new TuiReaderMainWindow(this, stories); + } + + public void open(MetaData meta) { + // TODO: open in editor + external option + if (true) { + if (!meta.isImageDocument()) { + new TuiReaderStoryWindow(this, meta); + } else { + messageBox("Error when trying to open the story", + "Images document not yet supported.", + 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); + } + } +} diff --git a/src/be/nikiroo/fanfix/reader/TuiReaderMainWindow.java b/src/be/nikiroo/fanfix/reader/TuiReaderMainWindow.java new file mode 100644 index 0000000..1de9488 --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/TuiReaderMainWindow.java @@ -0,0 +1,91 @@ +package be.nikiroo.fanfix.reader; + +import java.util.ArrayList; +import java.util.List; + +import jexer.TAction; +import jexer.TList; +import jexer.TRadioGroup; +import jexer.TTreeItem; +import jexer.TTreeView; +import jexer.TWindow; +import be.nikiroo.fanfix.data.MetaData; + +public class TuiReaderMainWindow extends TWindow { + private TList list; + private List listKeys; + private List listItems; + private TuiReaderApplication reader; + + /** + * Constructor. + * + * @param parent + * the main application + * @param flags + * bitmask of MODAL, CENTERED, or RESIZABLE + */ + public TuiReaderMainWindow(TuiReaderApplication reader, + List stories) { + // Construct a demo window. X and Y don't matter because it will be + // centered on screen. + super(reader, "Demo Window", 0, 0, 60, 18, CENTERED | RESIZABLE + | UNCLOSABLE); + + this.reader = reader; + + maximize(); + + listKeys = new ArrayList(); + listItems = new ArrayList(); + + if (stories != null) { + for (MetaData meta : stories) { + listKeys.add(meta); + listItems.add(desc(meta)); + } + } + + list = addList(listItems, 0, 0, getWidth(), getHeight(), new TAction() { + @Override + public void DO() { + if (list.getSelectedIndex() >= 0) { + enterOnStory(listKeys.get(list.getSelectedIndex())); + } + } + }); + + if (false) { + addLabel("Label (1,1)", 1, 1); + addButton("&Button (35,1)", 35, 1, new TAction() { + public void DO() { + } + }); + addCheckbox(1, 2, "Checky (1,2)", false); + addProgressBar(1, 3, 30, 42); + TRadioGroup groupy = addRadioGroup(1, 4, "Radio groupy"); + groupy.addRadioButton("Fanfan"); + groupy.addRadioButton("Tulipe"); + addField(1, 10, 20, false, "text not fixed."); + addField(1, 11, 20, true, "text fixed."); + addText("20x4 Text in (12,20)", 1, 12, 20, 4); + + TTreeView tree = addTreeView(30, 5, 20, 5); + TTreeItem root = new TTreeItem(tree, "expended root", true); + tree.setSelected(root); // needed to allow arrow navigation without + // mouse-clicking before + + root.addChild("child"); + root.addChild("child 2").addChild("sub child"); + + } + } + + private void enterOnStory(MetaData meta) { + reader.open(meta); + } + + private String desc(MetaData meta) { + return String.format("%5s: %s", meta.getLuid(), meta.getTitle()); + } +} diff --git a/src/be/nikiroo/fanfix/reader/TuiReaderStoryWindow.java b/src/be/nikiroo/fanfix/reader/TuiReaderStoryWindow.java new file mode 100644 index 0000000..47a644e --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/TuiReaderStoryWindow.java @@ -0,0 +1,60 @@ +package be.nikiroo.fanfix.reader; + +import jexer.TApplication; +import jexer.TText; +import jexer.TWindow; +import jexer.event.TResizeEvent; +import be.nikiroo.fanfix.Instance; +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 MetaData meta; + private Story story; + private TText textField; + + public TuiReaderStoryWindow(TApplication app, MetaData meta) { + super(app, desc(meta), 0, 0, 60, 18, CENTERED | RESIZABLE); + this.meta = meta; + + // /TODO: status bar with info, buttons to change chapter << < Chapter 0 + // : xxx.. > >> (max size for name = getWith() - X) + + // TODO: show all meta info before + + Chapter resume = getStory().getMeta().getResume(); + StringBuilder text = new StringBuilder(); + if (resume != null) { + // TODO: why does \n not work but \n\n do? bug in jexer? + text.append("Resume:\n\n "); // -> to status bar + for (Paragraph para : resume) { + text.append(para.getContent()).append("\n\n "); + } + } + textField = addText(text.toString(), 0, 0, getWidth(), getHeight()); + } + + @Override + public void onResize(TResizeEvent resize) { + super.onResize(resize); + + // Resize the text field + textField.setWidth(resize.getWidth()); + textField.setHeight(resize.getHeight()); + textField.reflow(); + } + + private Story getStory() { + if (story == null) { + // TODO: progress bar + story = Instance.getLibrary().getStory(meta.getLuid(), null); + } + return story; + } + + private static String desc(MetaData meta) { + return String.format("%s: %s", meta.getLuid(), meta.getTitle()); + } +}