diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..57b52ed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+bin
+target
+out
+.ant-targets-build.xml
+.classpath
+.project
+.settings
+.idea
+*.iml
diff --git a/build.fxbuild b/build.fxbuild
new file mode 100644
index 0000000..30b85e8
--- /dev/null
+++ b/build.fxbuild
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..35b5253
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maven-ant-tasks-2.1.3.jar b/maven-ant-tasks-2.1.3.jar
new file mode 100644
index 0000000..bec446f
Binary files /dev/null and b/maven-ant-tasks-2.1.3.jar differ
diff --git a/package/windows/Kiwi-setup-icon.bmp b/package/windows/Kiwi-setup-icon.bmp
new file mode 100644
index 0000000..b99b910
Binary files /dev/null and b/package/windows/Kiwi-setup-icon.bmp differ
diff --git a/package/windows/Kiwi-setup-icon.png b/package/windows/Kiwi-setup-icon.png
new file mode 100644
index 0000000..c861e74
Binary files /dev/null and b/package/windows/Kiwi-setup-icon.png differ
diff --git a/package/windows/Kiwi.ico b/package/windows/Kiwi.ico
new file mode 100644
index 0000000..be6fb03
Binary files /dev/null and b/package/windows/Kiwi.ico differ
diff --git a/package/windows/Kiwi.iss b/package/windows/Kiwi.iss
new file mode 100644
index 0000000..d1f429b
--- /dev/null
+++ b/package/windows/Kiwi.iss
@@ -0,0 +1,101 @@
+;This file will be executed next to the application bundle image
+;I.e. current directory will contain folder Kiwi with application files
+#define name "Kiwi"
+
+[Setup]
+AlwaysShowComponentsList=Yes
+AppId={{fxApplication}}
+AppName="{#name}"
+AppVersion=1.0
+AppVerName=Kiwi
+AppPublisher=Daniel Koudouna
+AppComments=kiwi
+AppCopyright=Copyright (C) 2016
+;AppPublisherURL=http://java.com/
+;AppSupportURL=http://java.com/
+;AppUpdatesURL=http://java.com/
+ChangesAssociations=Yes
+DefaultDirName="{pf}\{#name}"
+DisableStartupPrompt=Yes
+DisableDirPage=No
+DisableProgramGroupPage=No
+DisableReadyPage=No
+DisableFinishedPage=No
+DisableWelcomePage=No
+DefaultGroupName=kiwi
+;Optional License
+LicenseFile=
+;WinXP or above
+MinVersion=0,5.1
+OutputBaseFilename="{#name}-setup"
+Compression=lzma
+SolidCompression=yes
+PrivilegesRequired=admin
+SetupIconFile=Kiwi\Kiwi.ico
+UninstallDisplayIcon={app}\Kiwi.ico
+UninstallDisplayName=Kiwi
+WizardImageStretch=No
+WizardSmallImageFile=Kiwi-setup-icon.bmp
+ArchitecturesInstallIn64BitMode=x64
+
+[Tasks]
+Name: Association; Description: "Associate image file extensions ('.jpg','.png')"; GroupDescription: "File extensions"
+
+[Registry]
+Root: HKCR; Subkey: ".jpg"; ValueData: "{#name}"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" ; Tasks: Association
+Root: HKCR; Subkey: ".png"; ValueData: "{#name}"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" ; Tasks: Association
+Root: HKCR; Subkey: "{#name}"; ValueData: "{#name} Compatible File"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
+Root: HKCR; Subkey: "{#name}\DefaultIcon"; ValueData: "{app}\{#name}.exe,0"; ValueType: string; ValueName: ""
+Root: HKCR; Subkey: "{#name}\shell\open\command"; ValueData: """{app}\{#name}.exe"" ""%1"""; ValueType: string; ValueName: ""
+
+Root: HKCR; Subkey: "Applications\{#name}.exe"; ValueData: "Kiwi - A Lightweight Image Viewer"; ValueType: string; ValueName: "FriendlyAppName"
+
+[Types]
+Name: "full"; Description: "Full installation"
+Name: "compact"; Description: "Compact installation"
+Name: "custom"; Description: "Custom installation"; Flags: iscustom
+
+[Components]
+Name: "program"; Description: "Program Files"; Types: full compact custom; Flags: fixed
+Name: "group"; Description: "Add Start Menu Entry"; Types: full;
+Name: "icon"; Description: "Desktop Shortcut"; Types: full;
+
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Files]
+Source: "Kiwi\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: program;
+
+[Icons]
+Name: "{group}\Kiwi"; Filename: "{app}\Kiwi.exe"; IconFilename: "{app}\Kiwi.ico"; Components: group;
+Name: "{commondesktop}\Kiwi"; Filename: "{app}\Kiwi.exe"; IconFilename: "{app}\Kiwi.ico"; Components: icon;
+
+
+[Run]
+Filename: "{app}\Kiwi.exe"; Parameters: "-Xappcds:generatecache"; Check: returnFalse()
+Filename: "{app}\Kiwi.exe"; Description: "{cm:LaunchProgram,Kiwi}"; Flags: nowait postinstall skipifsilent; Check: returnTrue()
+Filename: "{app}\Kiwi.exe"; Parameters: "-install -svcName ""Kiwi"" -svcDesc ""Kiwi"" -mainExe ""Kiwi.exe"" "; Check: returnFalse()
+
+[UninstallRun]
+Filename: "{app}\Kiwi.exe "; Parameters: "-uninstall -svcName Kiwi -stopOnUninstall"; Check: returnFalse()
+
+[Code]
+function returnTrue(): Boolean;
+begin
+ Result := True;
+end;
+
+function returnFalse(): Boolean;
+begin
+ Result := False;
+end;
+
+function InitializeSetup(): Boolean;
+begin
+// Possible future improvements:
+// if version less or same => just launch app
+// if upgrade => check if same app is running and wait for it to exit
+// Add pack200/unpack200 support?
+ Result := True;
+end;
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..2bc69ac
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,70 @@
+
+ 4.0.0
+ kiwi
+ kiwi
+ 0.4
+ Kiwi
+
+ src
+
+
+ src
+
+ **/*.java
+
+
+
+
+
+ maven-compiler-plugin
+ 3.1
+
+
+ 1.8
+
+
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.3.1
+
+
+ com.github.junrar
+ junrar
+ 0.7
+
+
+ org.apache.commons
+ commons-io
+ 1.3.2
+
+
+ com.drewnoakes
+ metadata-extractor
+ 2.9.1
+
+
+ org.apache.ant
+ ant
+ 1.8.2
+
+
+ org.imgscalr
+ imgscalr-lib
+ 4.2
+
+
+ com.twelvemonkeys.imageio
+ imageio-jpeg
+ 3.3
+
+
+ com.twelvemonkeys.imageio
+ imageio-tiff
+ 3.3
+
+
+
\ No newline at end of file
diff --git a/src/com/proxy/kiwi/app/Kiwi.java b/src/com/proxy/kiwi/app/Kiwi.java
new file mode 100644
index 0000000..d4cce79
--- /dev/null
+++ b/src/com/proxy/kiwi/app/Kiwi.java
@@ -0,0 +1,61 @@
+package com.proxy.kiwi.app;
+
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.KiwiInstancer;
+import com.proxy.kiwi.core.utils.Resources;
+import com.proxy.kiwi.core.utils.Stopwatch;
+import com.proxy.kiwi.explorer.KiwiExplorerPane;
+import com.proxy.kiwi.reader.KiwiReadingPane;
+import javafx.scene.Scene;
+import javafx.scene.image.Image;
+import javafx.stage.Stage;
+
+import java.io.IOException;
+import java.util.List;
+
+public class Kiwi extends KiwiApplication {
+
+ public static void main(String[] args) {
+ if (args.length != 0) {
+ KiwiInstancer instancer = new KiwiInstancer();
+
+ try {
+ boolean woke = instancer.wakeIfExists(args);
+ if (woke) {
+ System.exit(0);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ launch(args);
+ }
+
+ protected void initialize(Stage stage) {
+ List params = getParameters().getUnnamed();
+
+ Scene scene;
+
+ if (params.size() > 0) {
+ Stopwatch.click("Loading reader");
+
+ String path = params.get(0);
+
+ scene = new Scene(new KiwiReadingPane(stage, path));
+
+ Stopwatch.click("Loading reader");
+ } else {
+ Stopwatch.click("Loading explorer");
+
+ scene = new Scene(new KiwiExplorerPane(stage, Config.getOption("path")));
+
+ Stopwatch.click("Loading explorer");
+ }
+
+ stage.getIcons().add(new Image(Resources.get("kiwi_small.png").toString()));
+ stage.setScene(scene);
+ stage.show();
+ }
+
+}
diff --git a/src/com/proxy/kiwi/app/KiwiApplication.java b/src/com/proxy/kiwi/app/KiwiApplication.java
new file mode 100644
index 0000000..07d374a
--- /dev/null
+++ b/src/com/proxy/kiwi/app/KiwiApplication.java
@@ -0,0 +1,148 @@
+package com.proxy.kiwi.app;
+
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.Instancer;
+import com.proxy.kiwi.core.services.KiwiInstancer;
+import com.proxy.kiwi.core.services.Thumbnails;
+import com.proxy.kiwi.core.utils.Log;
+import com.proxy.kiwi.core.utils.Resources;
+import com.proxy.kiwi.core.utils.Stopwatch;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.scene.text.Font;
+import javafx.stage.Stage;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.OverlappingFileLockException;
+
+import org.apache.tools.ant.util.JavaEnvUtils;
+
+public abstract class KiwiApplication extends Application {
+
+ @Override
+ public void start(Stage stage) throws Exception {
+
+ KiwiInstancer.setStage(stage);
+
+ Platform.setImplicitExit(false);
+ Config.init();
+ Thumbnails.init();
+
+ initialize(stage);
+
+ Resources.getAll("fonts/Ubuntu-L.ttf", "fonts/Ubuntu-B.ttf").forEach((font) -> {
+ Font.loadFont(font, 14);
+ });
+
+ stage.getScene().getStylesheets().addAll(Resources.getAll("application.css"));
+
+ stage.setOnCloseRequest((e) -> {
+ exit();
+ });
+
+ Platform.runLater(() -> {
+ addTrayIcon(stage);
+ });
+ }
+
+ protected abstract void initialize(Stage stage);
+
+ public static void exit() {
+ Config.save();
+ KiwiInstancer instancer = new KiwiInstancer();
+
+ try {
+ instancer.sleep();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (OverlappingFileLockException e) {
+ /*
+ * Nothing to do, since the file tried to re-lock itself. This means
+ * that the program was already sleeping.
+ */
+ }
+ }
+
+ public static void addTrayIcon(Stage stage) {
+ try {
+ SystemTray tray = SystemTray.getSystemTray();
+
+ if (tray.getTrayIcons().length == 0) {
+ javafx.scene.image.Image IMG = new javafx.scene.image.Image(
+ Resources.get("kiwi_small.png").openStream());
+
+ Image img = SwingFXUtils.fromFXImage(IMG, null);
+
+ int trayIconWidth = new TrayIcon(img).getSize().width;
+
+ TrayIcon icon = new TrayIcon(img.getScaledInstance(trayIconWidth, -1, Image.SCALE_SMOOTH));
+ icon.addActionListener(event -> {
+ KiwiInstancer instancer = new KiwiInstancer();
+ instancer.resume(Instancer.SELF_WAKE);
+ });
+
+ MenuItem showItem = new MenuItem("Show");
+ showItem.addActionListener(event -> {
+ KiwiInstancer instancer = new KiwiInstancer();
+ instancer.resume(Instancer.SELF_WAKE);
+ });
+
+ MenuItem hideItem = new MenuItem("Hide");
+ hideItem.addActionListener(event -> {
+ Platform.runLater(() -> {
+ stage.hide();
+ exit();
+ });
+ });
+
+ MenuItem exitItem = new MenuItem("Exit");
+ exitItem.addActionListener(event -> {
+ KiwiInstancer instancer = new KiwiInstancer();
+ instancer.shutdown();
+ });
+
+ PopupMenu popup = new PopupMenu();
+
+ popup.add(showItem);
+ popup.add(hideItem);
+ popup.add(exitItem);
+
+ File fontFile = Resources.getFile("fonts/Ubuntu-L.ttf", "font");
+
+ popup.setFont(java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, fontFile).deriveFont(16f));
+
+ icon.setPopupMenu(popup);
+
+ tray.add(icon);
+ }
+
+ } catch (Exception e) {
+ Log.print(e);
+ }
+
+ }
+
+ public static void startReader(String file) {
+ String classpath = System.getProperty("java.class.path");
+ String path = JavaEnvUtils.getJreExecutable("java");
+
+ ProcessBuilder processBuilder = new ProcessBuilder(path, "-cp", classpath, Kiwi.class.getName(), file);
+
+ processBuilder.inheritIO();
+
+ try {
+ Config.save();
+
+ processBuilder.start();
+ Stopwatch.click("Starting new JVM");
+
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/core/folder/FileComparators.java b/src/com/proxy/kiwi/core/folder/FileComparators.java
new file mode 100644
index 0000000..3acba6e
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/FileComparators.java
@@ -0,0 +1,120 @@
+package com.proxy.kiwi.core.folder;
+
+import java.io.File;
+import java.util.Comparator;
+
+public class FileComparators {
+
+ public static Comparator WINDOWS_LIKE = new Comparator() {
+
+ private String str1, str2;
+ private int pos1, pos2, len1, len2;
+
+ public int compare(File f1, File f2)
+ {
+
+ str1 = f1.getName();
+ str2 = f2.getName();
+ len1 = str1.length();
+ len2 = str2.length();
+ pos1 = pos2 = 0;
+
+ int result = 0;
+ while (result == 0 && pos1 < len1 && pos2 < len2)
+ {
+ char ch1 = str1.charAt(pos1);
+ char ch2 = str2.charAt(pos2);
+
+ if (Character.isDigit(ch1))
+ {
+ result = Character.isDigit(ch2) ? compareNumbers() : -1;
+ }
+ else if (Character.isLetter(ch1))
+ {
+ result = Character.isLetter(ch2) ? compareOther(true) : 1;
+ }
+ else
+ {
+ result = Character.isDigit(ch2) ? 1
+ : Character.isLetter(ch2) ? -1
+ : compareOther(false);
+ }
+
+ pos1++;
+ pos2++;
+ }
+
+ return result == 0 ? len1 - len2 : result;
+ }
+
+ private int compareNumbers()
+ {
+ int end1 = pos1 + 1;
+ while (end1 < len1 && Character.isDigit(str1.charAt(end1)))
+ {
+ end1++;
+ }
+ int fullLen1 = end1 - pos1;
+ while (pos1 < end1 && str1.charAt(pos1) == '0')
+ {
+ pos1++;
+ }
+
+ int end2 = pos2 + 1;
+ while (end2 < len2 && Character.isDigit(str2.charAt(end2)))
+ {
+ end2++;
+ }
+ int fullLen2 = end2 - pos2;
+ while (pos2 < end2 && str2.charAt(pos2) == '0')
+ {
+ pos2++;
+ }
+
+ int delta = (end1 - pos1) - (end2 - pos2);
+ if (delta != 0)
+ {
+ return delta;
+ }
+
+ while (pos1 < end1 && pos2 < end2)
+ {
+ delta = str1.charAt(pos1++) - str2.charAt(pos2++);
+ if (delta != 0)
+ {
+ return delta;
+ }
+ }
+
+ pos1--;
+ pos2--;
+
+ return fullLen2 - fullLen1;
+ }
+
+ private int compareOther(boolean isLetters)
+ {
+ char ch1 = str1.charAt(pos1);
+ char ch2 = str2.charAt(pos2);
+
+ if (ch1 == ch2)
+ {
+ return 0;
+ }
+
+ if (isLetters)
+ {
+ ch1 = Character.toUpperCase(ch1);
+ ch2 = Character.toUpperCase(ch2);
+ if (ch1 != ch2)
+ {
+ ch1 = Character.toLowerCase(ch1);
+ ch2 = Character.toLowerCase(ch2);
+ }
+ }
+
+ return ch1 - ch2;
+ }
+
+ };
+}
diff --git a/src/com/proxy/kiwi/core/folder/FileFolder.java b/src/com/proxy/kiwi/core/folder/FileFolder.java
new file mode 100644
index 0000000..8752f2c
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/FileFolder.java
@@ -0,0 +1,47 @@
+package com.proxy.kiwi.core.folder;
+
+import java.io.File;
+import java.util.Arrays;
+
+public class FileFolder extends Folder {
+
+ public FileFolder(String name, String path, Folder parent) {
+ super(name, path, parent);
+ }
+
+ @Override
+ public void load() {
+
+ isLoaded.set(true);
+
+ if (hasZipFolderParent()) {
+ getZipParent().load(this);
+ } else if (hasRarFolderParent()) {
+ getRarParent().load(this);
+ } else {
+ File file = new File(getFilenameProperty().get());
+ File[] files = file.listFiles();
+ Arrays.sort(files,FileComparators.WINDOWS_LIKE);
+ getVolumes().clear();
+ for (File child : files) {
+ if (Type.getType(child) == Type.IMAGE) {
+ addVolume(new Volume(child.getName(), child.getPath(), this));
+ }
+ }
+ }
+
+ }
+
+ @Override
+ protected File[] build() {
+ File file = new File(filename.get());
+ return file.listFiles();
+ }
+
+ @Override
+ protected String filterName(String name) {
+ return name.replaceAll("(\\(.*?\\))|(\\[.*?\\])|(\\{.*?\\})|(=.*?=)|(~.*?~)", "").replaceAll("\\A\\s*", "")
+ .trim();
+ }
+
+}
diff --git a/src/com/proxy/kiwi/core/folder/Folder.java b/src/com/proxy/kiwi/core/folder/Folder.java
new file mode 100644
index 0000000..3b23c7e
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/Folder.java
@@ -0,0 +1,386 @@
+package com.proxy.kiwi.core.folder;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.utils.Log;
+
+import javafx.application.Platform;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleStringProperty;
+
+public abstract class Folder {
+
+ protected SimpleIntegerProperty size;
+ protected SimpleStringProperty name, filename;
+ protected SimpleBooleanProperty isLoaded;
+ protected File file;
+
+ private Folder parent;
+ private Folder linkedFolder;
+ protected ArrayList volumes;
+ private ArrayList subfolders;
+ private ArrayList linkedFolders;
+
+ public Folder(String name, String path, Folder parent) {
+ this.name = new SimpleStringProperty(filterName(name));
+ this.filename = new SimpleStringProperty(path);
+
+ this.file = new File(path);
+
+ this.size = new SimpleIntegerProperty(0);
+ this.parent = parent;
+ this.linkedFolder = parent;
+ volumes = new ArrayList<>();
+ subfolders = new ArrayList<>();
+ linkedFolders = new ArrayList<>();
+ if (hasRarFolderParent()) {
+ getRarParent().getLinkedFolders().add(this);
+ }
+ if (hasZipFolderParent()) {
+ getZipParent().getLinkedFolders().add(this);
+ }
+ isLoaded = new SimpleBooleanProperty(false);
+
+ boolean containsImages = false;
+ File[] files = build();
+ Arrays.sort(files, FileComparators.WINDOWS_LIKE);
+
+ if (files != null) {
+
+ for (File child : files) {
+ if (child != null) {
+ Type type = Type.getType(child);
+ switch (type) {
+ case FOLDER:
+ addFolder(new FileFolder(child.getName(), child.getPath(), this));
+ break;
+ case RAR:
+// addFolder(new RarFolder(child.getName(), child.getPath(), this));
+ break;
+ case ZIP:
+// addFolder(new ZipFolder(child.getName(), child.getPath(), this));
+ break;
+ case IMAGE:
+ containsImages = true;
+ addVolume(new Volume(child.getName(), child.getPath(), this));
+ break;
+ default:
+ break;
+ }
+ }
+ if (containsImages) {
+ break;
+ }
+ }
+
+ if (!containsImages) {
+ Log.print(Log.PRELOAD,
+ "Finished building folder '" + getName() + "' with " + files.length + " objects.");
+ }
+
+ Collections.sort(subfolders, (first, second) -> {
+ return first.filename.get().compareTo(second.filename.get());
+ });
+ }
+ }
+
+ /**
+ * Loads the image contents of the folder into volumes.
+ */
+ public abstract void load();
+
+ /**
+ *
+ * @return a File[] with all the subfolders, and the minimum amount of
+ * Volumes.
+ */
+ // TODO replace with a better solution.
+ protected abstract File[] build();
+
+ protected abstract String filterName(String name);
+
+ public void addFolder(Folder folder) {
+ this.subfolders.add(folder);
+ }
+
+ public void addVolume(Volume volume) {
+ volumes.add(volume);
+ setSize(volumes.size());
+ }
+
+ public void clearVolumes() {
+ volumes.clear();
+ setSize(0);
+ }
+
+ /**
+ * Finds the appropriate image to display as a thumbnail for the Folder.
+ *
+ * @return the image path of the appropriate image.
+ */
+ public String getImagePath() {
+
+ if (Config.getFolderImage(name.get()) != null) {
+ // Search first in the settings.
+ return Config.getFolderImage(name.get());
+ } else if (hasVolumes()) {
+ // Search the folder itself for volumes.
+ return volumes.get(0).getFilename();
+ } else if (hasSubfolders()) {
+ // Search the subfolders for volumes.
+ return subfolders.get(0).getImagePath();
+ } else {
+ return null;
+ }
+ }
+
+ public SimpleIntegerProperty getSizeProperty() {
+ return size;
+ }
+
+ public String getName() {
+ return name.get();
+ }
+
+ public SimpleStringProperty getNameProperty() {
+ return name;
+ }
+
+ public SimpleStringProperty getFilenameProperty() {
+ return filename;
+ }
+
+ public SimpleBooleanProperty getIsLoadedProperty() {
+ return isLoaded;
+ }
+
+ public Folder getParent() {
+ return parent;
+ }
+
+ public ArrayList getVolumes() {
+ return volumes;
+ }
+
+ public ArrayList getSubfolders() {
+ return subfolders;
+ }
+
+ public ArrayList getLinkedFolders() {
+ return linkedFolders;
+ }
+
+ public boolean hasSubfolders() {
+ return !subfolders.isEmpty();
+ }
+
+ public boolean hasVolumes() {
+ return !volumes.isEmpty();
+ }
+
+ // TODO Find a better solution
+ public boolean hasZipFolderParent() {
+ return ((linkedFolder != null && linkedFolder instanceof ZipFolder)
+ || (parent != null && parent instanceof ZipFolder) || (parent != null && parent.hasZipFolderParent()));
+ }
+
+ // TODO Find a better solution
+ public boolean hasRarFolderParent() {
+ return ((linkedFolder != null && linkedFolder instanceof RarFolder)
+ || (parent != null && parent instanceof RarFolder) || (parent != null && parent.hasRarFolderParent()));
+ }
+
+ // TODO Find a better solution
+ public ZipFolder getZipParent() {
+ return (parent instanceof ZipFolder ? (ZipFolder) parent
+ : (linkedFolder instanceof ZipFolder ? (ZipFolder) linkedFolder : parent.getZipParent()));
+ }
+
+ // TODO Find a better solution
+ public RarFolder getRarParent() {
+ return (parent instanceof RarFolder ? (RarFolder) parent
+ : (linkedFolder instanceof RarFolder ? (RarFolder) linkedFolder : parent.getRarParent()));
+ }
+
+ public void setParent(Folder folder) {
+ this.parent = folder;
+ }
+
+ public File getFile() {
+ return file;
+ }
+
+ public void setSize(int n) {
+ /**
+ * Properties always have to be changed in the JavaFX Thread.
+ */
+ Platform.runLater(new Runnable() {
+ public void run() {
+ size.set(n);
+ }
+ });
+ }
+
+ /**
+ * @return true if the File is a direct child of this folder, false
+ * otherwise.
+ */
+ public boolean shouldContain(File file) {
+ // Check if the file path contains the file path of the folder.
+ if (file.getAbsolutePath().indexOf(filename.get()) > -1) {
+ return true;
+ } else {
+ // Check instead if the folder name is the same (Direct child).
+ return file.getParentFile().getName().indexOf(new File(filename.get()).getName()) > -1;
+ }
+ }
+
+ public boolean hasAncestor(Folder parent) {
+ return (this.parent != null) && (this.parent == parent || this.parent.hasAncestor(parent));
+ }
+
+ public boolean hasAncestorWith(String search) {
+ return (this.parent != null) && (this.parent.getName().toLowerCase().contains(search) || this.parent.hasAncestorWith(search));
+ }
+
+
+ public boolean equals(Object other) {
+ if (other == null || !other.getClass().getName().contains("Folder")) {
+ return false;
+ }
+ return file.equals(((Folder) other).getFile());
+
+ }
+
+ public Folder previous() {
+ File file = getFile();
+
+ Folder parent = Folder.fromFile(file.getParentFile());
+
+ // Folders.clean(parent);
+
+ Iterator iterator = parent.getSubfolders().iterator();
+
+ Folder prev = null;
+ while (iterator.hasNext()) {
+ Folder f = iterator.next();
+ if (f.equals(this)) {
+ if (prev != null) {
+ Folder fo = prev.getFirstImageFolder(false);
+ if (fo != null) {
+ fo.load();
+ return fo;
+ }
+ }
+ }
+ prev = f;
+ }
+
+ return parent.previous();
+ }
+
+ public Folder next() {
+ File file = getFile();
+
+ Folder parent = Folder.fromFile(file.getParentFile());
+
+ // Folders.clean(parent);
+ boolean found = false;
+
+ Iterator iterator = parent.getSubfolders().iterator();
+
+ while (iterator.hasNext()) {
+ Folder f = iterator.next();
+ if (found) {
+ Folder fo = f.getFirstImageFolder(true);
+ if (fo != null) {
+ fo.load();
+ return fo;
+ }
+ }
+ if (f.equals(this)) {
+ found = true;
+ }
+ }
+
+ return parent.next();
+ }
+
+ private Folder getFirstImageFolder(boolean forward) {
+ if (hasVolumes()) {
+ return this;
+ }
+ if (forward) {
+ for (Folder f : subfolders) {
+ Folder sub = f.getFirstImageFolder(forward);
+ if (sub != null) {
+ return sub;
+ }
+ }
+
+ } else {
+ for (int i = subfolders.size() - 1; i >= 0; i--) {
+ Folder f = subfolders.get(i);
+ Folder sub = f.getFirstImageFolder(forward);
+ if (sub != null) {
+ return sub;
+ }
+ }
+ }
+ return null;
+ }
+
+ public int find(String absolutePath) {
+ if (!getIsLoadedProperty().get()) {
+ load();
+ }
+
+ for (int i = 0; i < getVolumes().size(); i++) {
+ Volume v = getVolumes().get(i);
+ if (v.getFilename().equals(filename)) {
+ return i + 1;
+ }
+ }
+
+ return 1;
+ }
+
+ /**
+ * ABSTRACT METHODS
+ */
+
+ public static Folder fromFile(String path) {
+ return fromFile(new File(path));
+ }
+
+ public static Folder fromFile(File file) {
+ switch (Type.getType(file)) {
+ case IMAGE:
+ file = file.getParentFile();
+ return new FileFolder(file.getName(), file.getAbsolutePath(), null);
+ case OTHER:
+ break;
+ case RAR:
+ return new RarFolder(file.getName(), file.getAbsolutePath(), null);
+ case SZ:
+ break;
+ case TAR:
+ break;
+ case ZIP:
+ return new ZipFolder(file.getName(), file.getAbsolutePath(), null);
+ case FOLDER:
+ return new FileFolder(file.getName(), file.getAbsolutePath(), null);
+ default:
+ break;
+
+ }
+ return null;
+ }
+
+}
diff --git a/src/com/proxy/kiwi/core/folder/RarFolder.java b/src/com/proxy/kiwi/core/folder/RarFolder.java
new file mode 100644
index 0000000..82e7137
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/RarFolder.java
@@ -0,0 +1,162 @@
+package com.proxy.kiwi.core.folder;
+
+import com.github.junrar.Archive;
+import com.github.junrar.exception.RarException;
+import com.github.junrar.rarfile.FileHeader;
+import com.proxy.kiwi.core.services.Folders;
+import com.proxy.kiwi.core.utils.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * Folder created with the contents of a .rar file. The .rar file requires an
+ * external library to extract, as it is a proprietary format.
+ *
+ * @author Daniel
+ *
+ */
+// TODO make new class archive folder with refactored loading
+public class RarFolder extends Folder {
+
+ public File outputDir;
+ /*
+ * HashMap containing all the children's loaded status. Independent of the
+ * Folder's loaded property, since they are loaded through the parent.
+ */
+ HashMap loaded;
+
+ public RarFolder(String name, String path, Folder parent) {
+ super(name, path, parent);
+ loaded = new HashMap<>();
+ }
+
+ @Override
+ public void load() {
+ load(this);
+ }
+
+ /**
+ * Load a folder inside the rar file, which could be the rar file itself.
+ *
+ * @param folder
+ * the folder in question
+ */
+ public void load(Folder folder) {
+ if (loaded.get(folder) != null && loaded.get(folder).equals(Boolean.TRUE)) {
+ return;
+ }
+ loaded.put(folder, Boolean.TRUE);
+ new Thread(new UnrarRunner(this, folder)).start();
+ }
+
+ @Override
+ protected File[] build() {
+ File file = new File(filename.get());
+ try {
+ outputDir = Files.createTempDirectory(Folders.getTempPath(), file.getName()).toFile();
+ } catch (IOException e1) {
+ // TODO error message? probably privilages
+ e1.printStackTrace();
+ }
+
+ try {
+ Archive a = new Archive(file);
+
+ for (FileHeader header : a.getFileHeaders()) {
+ File entryDestination = new File(outputDir, header.getFileNameString());
+ if (header.isDirectory()) {
+ entryDestination.mkdirs();
+ } else {
+ entryDestination.getParentFile().mkdirs();
+ int imgs = 0;
+ if (entryDestination.getParentFile() != null) {
+ for (File f : entryDestination.getParentFile().listFiles()) {
+ if (!f.isDirectory()) {
+ imgs++;
+ }
+ }
+ }
+
+ if (imgs < 2 && Type.getType(header) == Type.IMAGE) {
+ OutputStream out = new FileOutputStream(entryDestination);
+ a.extractFile(header, out);
+ out.close();
+ }
+
+ }
+ }
+ a.close();
+ } catch (RarException | IOException e) {
+ // TODO error message or something
+ e.printStackTrace();
+ }
+ return outputDir.listFiles();
+ }
+
+ @Override
+ protected String filterName(String name) {
+ return name.replaceAll("(\\(.*?\\))|(\\[.*?\\])|(\\{.*?\\})|(=.*?=)|(~.*?~)", "").replaceAll("\\A\\s*", "")
+ .replaceAll(".rar", "").trim();
+ }
+
+ protected static boolean zipFileIsImage(String name) {
+ return name.contains(".jpg") || name.contains(".gif") || name.contains(".png");
+ }
+}
+
+class UnrarRunner implements Runnable {
+
+ RarFolder folder;
+ Folder target;
+
+ public UnrarRunner(RarFolder folder, Folder target) {
+ this.folder = folder;
+ this.target = target;
+ }
+
+ @Override
+ public void run() {
+ target.clearVolumes();
+
+ int fileCount = 0;
+
+ try {
+ File file = new File(folder.getFilenameProperty().get());
+ Archive a = new Archive(file);
+ Log.print(Log.IO, "Loading folder '" + target.getName() + "' from .rar file '" + folder.getName() + "'");
+ for (FileHeader header : a.getFileHeaders()) {
+ File entryDestination = new File(folder.outputDir, header.getFileNameString());
+ if (header.isDirectory()) {
+ entryDestination.mkdirs();
+ } else if (Type.getType(header) == Type.IMAGE) {
+ entryDestination.getParentFile().mkdirs();
+
+ if (target.shouldContain(entryDestination)) {
+
+ OutputStream out = new FileOutputStream(entryDestination);
+ a.extractFile(header, out);
+ out.close();
+ fileCount++;
+ target.addVolume(
+ new Volume(entryDestination.getName(), entryDestination.getAbsolutePath(), target));
+ }
+
+ }
+ }
+ a.close();
+ Log.print(Log.IO, "Extracted " + fileCount + " files for " + target.getName());
+ } catch (RarException | IOException e) {
+ // TODO error message
+ e.printStackTrace();
+ }
+
+ Collections.sort(target.getVolumes());
+ }
+
+}
diff --git a/src/com/proxy/kiwi/core/folder/Type.java b/src/com/proxy/kiwi/core/folder/Type.java
new file mode 100644
index 0000000..5981e12
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/Type.java
@@ -0,0 +1,48 @@
+package com.proxy.kiwi.core.folder;
+
+import com.github.junrar.rarfile.FileHeader;
+
+import java.io.File;
+import java.util.zip.ZipEntry;
+
+public enum Type {
+ FOLDER, IMAGE, RAR, ZIP, SZ, TAR, OTHER;
+
+ public static Type getType(FileHeader header) {
+ if (header.isDirectory()) {
+ return FOLDER;
+ } else {
+ return getType(header.getFileNameString());
+ }
+ }
+
+ public static Type getType(ZipEntry entry) {
+ if (entry.isDirectory()) {
+ return FOLDER;
+ } else {
+ return getType(entry.getName());
+ }
+ }
+
+ public static Type getType(File file) {
+ if (file.isDirectory()) {
+ return FOLDER;
+ } else {
+ return getType(file.getAbsolutePath());
+ }
+ }
+
+ public static Type getType(String fn) {
+ fn = fn.toLowerCase();
+ if (fn.endsWith(".jpg") || fn.endsWith(".png") || fn.endsWith(".gif")) {
+ return IMAGE;
+ }
+ if (fn.endsWith(".rar")) {
+ return RAR;
+ }
+ if (fn.endsWith(".zip") || fn.endsWith(".7z") || fn.endsWith(".tar") || fn.endsWith(".tar.gz")) {
+ return ZIP;
+ }
+ return OTHER;
+ }
+}
diff --git a/src/com/proxy/kiwi/core/folder/Volume.java b/src/com/proxy/kiwi/core/folder/Volume.java
new file mode 100644
index 0000000..07fbea7
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/Volume.java
@@ -0,0 +1,32 @@
+package com.proxy.kiwi.core.folder;
+
+import java.io.File;
+
+public class Volume implements Comparable {
+
+ private String name, path;
+ private File file;
+
+ public Volume(String name, String path, Folder folder) {
+ this.name = name;
+ this.path = path;
+ this.file = new File(path);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getFilename() {
+ return path;
+ }
+
+ public File getFile() {
+ return file;
+ }
+
+ @Override
+ public int compareTo(Volume other) {
+ return getName().compareTo(other.getName());
+ }
+}
diff --git a/src/com/proxy/kiwi/core/folder/ZipFolder.java b/src/com/proxy/kiwi/core/folder/ZipFolder.java
new file mode 100644
index 0000000..3ec7e57
--- /dev/null
+++ b/src/com/proxy/kiwi/core/folder/ZipFolder.java
@@ -0,0 +1,131 @@
+package com.proxy.kiwi.core.folder;
+
+import org.apache.commons.io.IOUtils;
+
+import com.proxy.kiwi.core.services.Folders;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class ZipFolder extends Folder {
+
+ public File outputDir;
+ HashMap loaded;
+
+ public ZipFolder(String name, String path, Folder parent) {
+ super(name, path, parent);
+ loaded = new HashMap<>();
+ }
+
+ @Override
+ public void load() {
+ load(this);
+ }
+
+ public void load(Folder folder) {
+ if (loaded.get(folder) != null && loaded.get(folder).equals(Boolean.TRUE)) {
+ return;
+ }
+ loaded.put(folder, Boolean.TRUE);
+ new Thread(new UnzipRunner(this, folder)).start();
+ }
+
+ @Override
+ protected File[] build() {
+ try {
+ File file = new File(filename.get());
+ outputDir = Files.createTempDirectory(Folders.getTempPath(), file.getName()).toFile();
+
+ ZipFile zipFile = null;
+ zipFile = new ZipFile(file);
+
+ boolean hasLoaded = false;
+
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements() && !hasLoaded) {
+ ZipEntry entry = entries.nextElement();
+ File entryDestination = new File(outputDir, entry.getName());
+ if (entry.isDirectory()) {
+ entryDestination.mkdirs();
+ } else {
+ entryDestination.getParentFile().mkdirs();
+
+ InputStream in = zipFile.getInputStream(entry);
+ OutputStream out = new FileOutputStream(entryDestination);
+ IOUtils.copy(in, out);
+ IOUtils.closeQuietly(in);
+ out.close();
+
+ addVolume(new Volume(entryDestination.getName(), entryDestination.getAbsolutePath(), this));
+
+ }
+ }
+ zipFile.close();
+ } catch (IOException e) {
+
+ }
+ return outputDir.listFiles();
+
+ }
+
+ @Override
+ protected String filterName(String name) {
+ return name;
+ }
+
+}
+
+class UnzipRunner implements Runnable {
+
+ ZipFolder folder;
+ Folder target;
+
+ UnzipRunner(ZipFolder folder, Folder target) {
+ this.folder = folder;
+ this.target = target;
+ }
+
+ @Override
+ public void run() {
+
+ try {
+ target.clearVolumes();
+ File file = new File(folder.filename.get());
+
+ ZipFile zipFile = null;
+ zipFile = new ZipFile(file);
+
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ File entryDestination = new File(folder.outputDir, entry.getName());
+ if (entry.isDirectory()) {
+ entryDestination.mkdirs();
+ } else {
+ entryDestination.getParentFile().mkdirs();
+
+ InputStream in = zipFile.getInputStream(entry);
+ OutputStream out = new FileOutputStream(entryDestination);
+ IOUtils.copy(in, out);
+ IOUtils.closeQuietly(in);
+ out.close();
+
+ target.addVolume(
+ new Volume(entryDestination.getName(), entryDestination.getAbsolutePath(), target));
+
+ }
+ }
+ zipFile.close();
+ } catch (IOException e) {
+
+ }
+
+ Collections.sort(target.getVolumes());
+
+ }
+}
diff --git a/src/com/proxy/kiwi/core/image/Cache.java b/src/com/proxy/kiwi/core/image/Cache.java
new file mode 100644
index 0000000..6100bdf
--- /dev/null
+++ b/src/com/proxy/kiwi/core/image/Cache.java
@@ -0,0 +1,74 @@
+package com.proxy.kiwi.core.image;
+
+import com.proxy.kiwi.core.utils.Log;
+
+/**
+ * Image cache for JavaFX image loading.
+ *
+ * @author Daniel
+ *
+ */
+public class Cache {
+ private int size;
+
+ private int index;
+
+ String[] keys;
+ KiwiImage[] images;
+
+ public Cache(int size) {
+ this.size = size;
+ keys = new String[size];
+ images = new KiwiImage[size];
+ index = 0;
+ }
+
+ public void add(String key, KiwiImage value) {
+
+ synchronized (keys) {
+
+ if (images[index] != null) {
+ images[index] = null;
+ Log.print(Log.IO, "Removing to cache: " + keys[index]);
+ }
+
+ Log.print(Log.IO, "Adding to cache: " + key);
+
+ keys[index] = key;
+ images[index] = value;
+
+ index = (index + 1) % size;
+ }
+
+ /* "Java can manage heap space just fine" */
+ System.gc();
+ }
+
+ public boolean contains(String name) {
+ for (String key : keys) {
+ if (key != null && key.equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public KiwiImage get(String key) {
+ synchronized (keys) {
+ for (int i = 0; i < size; i++) {
+ if (keys[i].equals(key)) {
+ return images[i];
+ }
+ }
+ }
+ return null;
+ }
+
+ public void clear() {
+ for (int i = 0; i < size; i++) {
+ keys[i] = "-1";
+ images[i] = null;
+ }
+ System.gc();
+ }
+}
diff --git a/src/com/proxy/kiwi/core/image/KiwiImage.java b/src/com/proxy/kiwi/core/image/KiwiImage.java
new file mode 100644
index 0000000..bb8cc82
--- /dev/null
+++ b/src/com/proxy/kiwi/core/image/KiwiImage.java
@@ -0,0 +1,63 @@
+package com.proxy.kiwi.core.image;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.drew.imaging.ImageMetadataReader;
+import com.drew.imaging.ImageProcessingException;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.Tag;
+
+import javafx.scene.image.Image;
+
+public class KiwiImage extends Image {
+
+ private Orientation orientation;
+ private Metadata metadata;
+
+ public KiwiImage(File file) {
+ this(file, 0, 0, false, true, false);
+ }
+
+ public KiwiImage(File file, double width, double height, boolean preserveRatio, boolean smooth,
+ boolean backgroundLoading) {
+ super(file.toURI().toString(), width, height, preserveRatio, smooth, backgroundLoading);
+ this.metadata = getMetadata(file);
+ this.orientation = setOrientation();
+ }
+
+ public Orientation getOrientation() {
+ return orientation;
+ }
+
+ private Orientation setOrientation() {
+ if (metadata == null) {
+ return Orientation.NONE;
+ }
+
+ for (Directory dir : metadata.getDirectories()) {
+ for (Tag tag : dir.getTags()) {
+ if (tag.getTagName().equals("Orientation")) {
+ String description = tag.getDescription();
+ if (description.contains("Rotate 90 CW")) {
+ return Orientation.CW_HALF;
+ }
+ // TODO find other rotation descriptions
+ break;
+ }
+ }
+ }
+
+ return Orientation.NONE;
+ }
+
+ private Metadata getMetadata(File file) {
+ try {
+ return ImageMetadataReader.readMetadata(file);
+ } catch (ImageProcessingException | IOException e) {
+ return null;
+ }
+ }
+
+}
diff --git a/src/com/proxy/kiwi/core/image/Orientation.java b/src/com/proxy/kiwi/core/image/Orientation.java
new file mode 100644
index 0000000..3c9c740
--- /dev/null
+++ b/src/com/proxy/kiwi/core/image/Orientation.java
@@ -0,0 +1,40 @@
+package com.proxy.kiwi.core.image;
+
+public enum Orientation {
+ NONE(0), CW_HALF(90), CCW_HALF(270), FULL(180);
+
+ private double rotation;
+
+ Orientation(double rotation) {
+ this.rotation = rotation;
+ }
+
+ public double getRotation() {
+ return rotation;
+ }
+
+ public double getWidthRatio(double width, double height) {
+ switch (this) {
+ case CCW_HALF:
+ case CW_HALF:
+ return width / height;
+ case FULL:
+ case NONE:
+ default:
+ return 1.0;
+ }
+ }
+
+ public double getHeightRatio(double width, double height) {
+ switch (this) {
+ case CCW_HALF:
+ case CW_HALF:
+ return height / width;
+ case FULL:
+ case NONE:
+ default:
+ return 1.0;
+
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/core/services/Config.java b/src/com/proxy/kiwi/core/services/Config.java
new file mode 100644
index 0000000..8d0a06e
--- /dev/null
+++ b/src/com/proxy/kiwi/core/services/Config.java
@@ -0,0 +1,598 @@
+package com.proxy.kiwi.core.services;
+
+import com.google.gson.*;
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.utils.Command;
+import com.proxy.kiwi.core.utils.FileListener;
+import com.proxy.kiwi.core.utils.Log;
+import com.proxy.kiwi.core.utils.Resources;
+
+import javafx.scene.input.KeyCode;
+
+import java.io.*;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.Map.Entry;
+
+public class Config {
+
+ private static JsonObject config;
+
+ private static String config_path, default_config;
+
+ private static boolean create_flag = false;
+
+ private static FileListener listener;
+
+ public static void init() {
+ String OS = (System.getProperty("os.name")).toUpperCase();
+ String workingDirectory;
+ String defaultPath;
+
+ /* If on windows, use the AppData folder. */
+ if (OS.contains("WIN")) {
+ workingDirectory = System.getenv("AppData") + "\\Kiwi\\";
+ defaultPath = Paths.get("C:", "Users", System.getProperty("user.name"), "Pictures").toString();
+
+ }
+ /* Else, use the home directory. */
+ else {
+ workingDirectory = System.getProperty("user.home") + "/Kiwi/";
+ defaultPath = System.getProperty("user.home") + "/Pictures/";
+ // TODO On Macs, add System.getProperty("user.home") +
+ // "/Library/Application Support/Kiwi/"
+ }
+
+ File folder = new File(workingDirectory);
+ folder.mkdir();
+
+ config_path = workingDirectory + "config.json";
+
+ default_config = Resources.getContent("default_config.txt").replaceAll("_PATH_", defaultPath);
+
+ JsonParser parser = new JsonParser();
+
+ try {
+ config = parser.parse(new FileReader(config_path)).getAsJsonObject();
+ } catch (JsonIOException | JsonSyntaxException e) {
+ // TODO error message
+ e.printStackTrace();
+ } catch (FileNotFoundException e) {
+
+ config = parser.parse(default_config).getAsJsonObject();
+ setOption("path", defaultPath);
+ addLibrary(defaultPath);
+
+ }
+
+ addMissingHotkeys();
+
+ if (listener == null) {
+ listener = FileListener.create(config_path, () -> {
+ Log.print(Log.IO, "Detected change in config file");
+ init();
+ });
+ }
+
+
+ }
+
+ public static void save() {
+ clean();
+ try {
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ JsonParser jp = new JsonParser();
+ FileWriter fOut = new FileWriter(config_path);
+ fOut.write(gson.toJson(jp.parse(config.toString())));
+ fOut.close();
+ } catch (IOException e) {
+ // TODO error message
+ e.printStackTrace();
+ }
+ }
+
+ private static void clean() {
+ JsonObject folders = config.get("folders").getAsJsonObject();
+ for (Iterator> iterator = folders.entrySet().iterator(); iterator.hasNext();) {
+ Entry entry = iterator.next();
+ if (entry.getValue().getAsJsonObject().entrySet().size() == 0) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public static String getOption(String key) {
+ return config.get(key).getAsString();
+ }
+
+ public static int getIntOption(String key) {
+ return config.get(key).getAsInt();
+ }
+
+ public static boolean getBoolOption(String key) {
+ return config.get(key).getAsBoolean();
+ }
+
+ public static void setOption(String key, String value) {
+ config.addProperty(key, value);
+ }
+
+ public static void setIntOption(String key, int value) {
+ config.addProperty(key, value);
+ }
+
+ public static void setBoolOption(String key, boolean value) {
+ config.addProperty(key, value);
+ }
+
+ public static String getFolderImage(String folder) {
+ if (!propertyExists(folder, "image")) {
+ return null;
+ }
+ return getFolder(folder).get("image").getAsString();
+ }
+
+ public static void setFolderImage(String folder, String path) {
+ checkFolderExists(folder);
+ getFolder(folder).addProperty("image", path);
+ }
+
+ public static int getFolderXOffset(String folder) {
+ if (!propertyExists(folder, "xOff")) {
+ return 0;
+ }
+ return getFolder(folder).get("xOff").getAsInt();
+ }
+
+ public static void setFolderXOffset(String folder, int off) {
+ checkFolderExists(folder);
+ getFolder(folder).addProperty("xOff", off);
+ }
+
+ public static int getFolderYOffset(String folder) {
+ if (!propertyExists(folder, "yOff")) {
+ return 0;
+ }
+ return getFolder(folder).get("yOff").getAsInt();
+ }
+
+ public static void setFolderYOffset(String folder, int off) {
+ checkFolderExists(folder);
+ getFolder(folder).addProperty("yOff", off);
+ }
+
+ public static void addFolderChapter(String folder, int page) {
+ checkFolderExists(folder);
+ if (propertyExists(folder, "chapters")) {
+ String[] chapters = getChapters(folder);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < chapters.length; i++) {
+ sb.append(chapters[i]);
+
+ if (page > Integer.parseInt(chapters[i].trim())
+ && ((i + 1 < chapters.length && page < Integer.parseInt(chapters[i + 1].trim()))
+ || i + 1 == chapters.length)) {
+ sb.append(",");
+ sb.append(page);
+ }
+ if (i != chapters.length - 1) {
+ sb.append(",");
+ }
+ }
+ getFolder(folder).addProperty("chapters", sb.toString());
+ } else {
+ if (page == 1) {
+ getFolder(folder).addProperty("chapters", "" + 1);
+ } else {
+ getFolder(folder).addProperty("chapters", "1," + page);
+ }
+ }
+ save();
+ }
+
+ public static void removeFolderChapter(String folder, int page) {
+ checkFolderExists(folder);
+ if (propertyExists(folder, "chapters")) {
+ StringBuilder sb = new StringBuilder();
+ String[] chapters = getChapters(folder);
+ for (int i = 0; i < chapters.length; i++) {
+ if (Integer.parseInt(chapters[i].trim()) != page) {
+ sb.append(chapters[i]);
+ if (i != chapters.length - 1) {
+ sb.append(",");
+ }
+ }
+ }
+ if (sb.toString().length() == 0) {
+ getFolder(folder).remove("chapers");
+ } else {
+ getFolder(folder).addProperty("chapters", sb.toString());
+ }
+ }
+ save();
+ }
+
+ public static int getNextChapter(String folder, int page) {
+ String[] chapters = getChapters(folder);
+ int[] array = new int[chapters.length];
+ for (int i = 0; i < array.length; i++) {
+ try {
+ array[i] = Integer.parseInt(chapters[i].trim());
+ } catch (NumberFormatException e) {
+
+ }
+ }
+ for (int i = 0; i < array.length; i++) {
+ if (page >= array[i] && i + 1 < array.length && page < array[i + 1]) {
+ return array[i + 1];
+ }
+ }
+ return 1;
+ }
+
+ public static int getPreviousChapter(String folder, int page) {
+ String[] chapters = getChapters(folder);
+ int[] array = new int[chapters.length];
+ for (int i = 0; i < array.length; i++) {
+ try {
+ array[i] = Integer.parseInt(chapters[i].trim());
+ } catch (NumberFormatException e) {
+
+ }
+ }
+ for (int i = 0; i < array.length; i++) {
+ if (page > array[i] && i + 1 < array.length && page <= array[i + 1]) {
+ return array[i];
+ }
+ }
+ return array[array.length - 1];
+ }
+
+ public static String[] getChapters(String folder) {
+ checkFolderExists(folder);
+ if (propertyExists(folder, "chapters")) {
+ return getFolder(folder).get("chapters").getAsString().split(",");
+ } else {
+ return null;
+ }
+ }
+
+ private static void checkFolderExists(String folder) {
+ JsonElement je;
+ je = config.get("folders");
+ if (je == null) {
+ config.add("folders", new JsonObject());
+ }
+ je = config.get("folders").getAsJsonObject().get(folder);
+ if (je == null) {
+ config.get("folders").getAsJsonObject().add(folder, new JsonObject());
+ }
+ }
+
+ private static boolean propertyExists(String folder, String property) {
+ return (config.get("folders") != null && config.get("folders").getAsJsonObject().get(folder) != null
+ && getFolder(folder).get(property) != null);
+ }
+
+ private static JsonObject getFolder(String folder) {
+ return config.get("folders").getAsJsonObject().get(folder).getAsJsonObject();
+ }
+
+ public static JsonArray getHotkeysFor(String command) {
+ return config.getAsJsonObject("hotkeys").getAsJsonArray(command);
+ }
+
+ private static void addMissingHotkeys() {
+ Gson gson = new Gson();
+ JsonObject hotkeys = config.getAsJsonObject("hotkeys");
+
+ for (Command command : Command.values()) {
+ if (!hotkeys.entrySet().contains(command.getName())) {
+ if (command.default_hotkeys.length > 0) {
+ JsonArray array = new JsonArray();
+ for (String hotkey : command.default_hotkeys) {
+ JsonElement el = gson.fromJson(hotkey, JsonElement.class);
+ array.add(el);
+ }
+ hotkeys.add(command.getName(), array);
+ }
+ }
+ }
+ }
+
+ public static Command getCommandFor(KeyCode key) {
+ for (Entry entry : config.getAsJsonObject("hotkeys").entrySet()) {
+ JsonArray arr = entry.getValue().getAsJsonArray();
+ for (JsonElement hotkey : arr) {
+ if (hotkey != null) {
+ if (KeyCode.getKeyCode(hotkey.getAsString()).equals(key)) {
+ return Command.get(entry.getKey());
+ }
+ }
+ }
+ }
+ ;
+
+ return Command.UNDEFINED;
+ }
+
+ public static Set> getHotkeys() {
+ return config.getAsJsonObject("hotkeys").entrySet();
+ }
+
+ public static Set> getLibraries() {
+ return config.getAsJsonObject("libraries").entrySet();
+ }
+
+ public static void addLibrary(String path) {
+ Gson gson = new Gson();
+ JsonElement el = gson.fromJson("false", JsonElement.class);
+ JsonElement el2 = gson.fromJson("true", JsonElement.class);
+
+ config.getAsJsonObject("libraries").entrySet().forEach((entry) -> {
+ if (entry.getKey().equals(path)) {
+ entry.setValue(el2);
+ } else {
+ entry.setValue(el);
+ }
+
+ });
+
+ if (!config.getAsJsonObject("libraries").has(path)) {
+ config.getAsJsonObject("libraries").add(path, el);
+ }
+ save();
+ }
+
+ public static Set getArtists() {
+ Set artists = new HashSet<>();
+ for (Entry folder : config.get("folders").getAsJsonObject().entrySet()) {
+ JsonElement folderArtists = folder.getValue().getAsJsonObject().get("artists");
+ if (folderArtists != null) {
+ folderArtists.getAsJsonArray().forEach((el) -> {
+ artists.add(el.getAsString());
+ });
+ }
+ }
+
+ return artists;
+ }
+
+ public static Set getArtists(Folder folder) {
+ Set artists = new HashSet<>();
+ checkFolderExists(folder.getName());
+ if (config.get("folders").getAsJsonObject().get(folder.getName()) != null) {
+ JsonElement folderArtists = getFolder(folder.getName()).getAsJsonObject().get("tags");
+ if (folderArtists != null) {
+ folderArtists.getAsJsonArray().forEach((el) -> {
+ artists.add(el.getAsString());
+ });
+ }
+ }
+ return artists;
+ }
+
+ public static Set getTags() {
+ Set tags = new HashSet<>();
+ for (Entry folder : config.get("folders").getAsJsonObject().entrySet()) {
+ JsonElement folderTags = folder.getValue().getAsJsonObject().get("tags");
+ if (folderTags != null) {
+ folderTags.getAsJsonArray().forEach((el) -> {
+ tags.add(el.getAsString());
+ });
+ }
+ }
+
+ return tags;
+ }
+
+ public static Set getTags(Folder folder) {
+ Set tags = new HashSet<>();
+ if (config.get("folders").getAsJsonObject().get(folder.getName()) != null) {
+ JsonElement folderTags = getFolder(folder.getName()).getAsJsonObject().get("tags");
+ if (folderTags != null) {
+ folderTags.getAsJsonArray().forEach((el) -> {
+ tags.add(el.getAsString());
+ });
+ }
+ }
+ return tags;
+ }
+
+ public static void setTags(Folder folder, Set tags) {
+ Gson gson = new Gson();
+ checkFolderExists(folder.getName());
+ JsonObject f = getFolder(folder.getName());
+ JsonElement folderTags = f.get("tags");
+ if (folderTags != null) {
+ f.remove("tags");
+ }
+ f.add("tags", new JsonArray());
+ folderTags = f.get("tags");
+ for (String tag : tags) {
+ folderTags.getAsJsonArray().add(gson.fromJson(tag, JsonElement.class));
+ }
+ save();
+ }
+
+ public static void setArtists(Folder folder, Set artists) {
+ Gson gson = new Gson();
+ checkFolderExists(folder.getName());
+ JsonObject f = getFolder(folder.getName());
+
+ JsonElement folderArtists = f.get("artists");
+ if (folderArtists != null) {
+ f.remove("artists");
+ }
+ f.add("artists", new JsonArray());
+ folderArtists = f.get("artists");
+
+ for (String artist : artists) {
+ folderArtists.getAsJsonArray().add(gson.fromJson(artist, JsonElement.class));
+ }
+
+ }
+
+ // New API
+
+ public static Set aggregateStringArrays(String key, String... keys) {
+ Set set = new HashSet<>();
+
+ JsonElement obj = get(keys);
+ if (obj != null && obj instanceof JsonObject) {
+ strFullMap(set, obj.getAsJsonObject(), key);
+ }
+
+ return set;
+ }
+
+ private static void strFullMap(Set set, JsonObject obj, String key) {
+ obj.entrySet().forEach((entry) -> {
+ if (entry.getKey().equals(key) && entry.getValue() instanceof JsonArray) {
+ entry.getValue().getAsJsonArray().forEach((el) -> {
+ set.add(el.getAsString());
+ });
+ } else if (entry.getValue() instanceof JsonObject) {
+ strFullMap(set, entry.getValue().getAsJsonObject(), key);
+ }
+ });
+ }
+
+ public static Map intArrayMap(String... keys) {
+ Map map = new HashMap<>();
+ JsonElement obj = get(keys);
+ if (obj == null) {
+ return map;
+ }
+ obj.getAsJsonObject().entrySet().forEach((entry) -> {
+ map.put(entry.getKey(), toIntArray(entry.getValue().getAsJsonArray()));
+ });
+
+ return map;
+ }
+
+ public static Map arrayMap(String... keys) {
+ Map map = new HashMap();
+ JsonElement obj = get(keys);
+ if (obj == null) {
+ return map;
+ }
+ obj.getAsJsonObject().entrySet().forEach((entry) -> {
+ map.put(entry.getKey(), toArray(entry.getValue().getAsJsonArray()));
+ });
+
+ return map;
+ }
+
+ public static Map stringMap(String... keys) {
+ Map map = new HashMap();
+ JsonElement obj = get(keys);
+ if (obj == null) {
+ return map;
+ }
+ obj.getAsJsonObject().entrySet().forEach((entry) -> {
+ map.put(entry.getKey(), entry.getValue().getAsString());
+ });
+
+ return map;
+ }
+
+ public static Set stringSet(String... keys) {
+ Set res = new HashSet<>();
+ JsonArray arr = get(keys).getAsJsonArray();
+
+ if (arr == null) {
+ return null;
+ }
+ arr.forEach((el) -> {
+ res.add(el.getAsString());
+ });
+
+ return res;
+ }
+
+ public static int[] intArray(String... keys) {
+ return toIntArray(get(keys).getAsJsonArray());
+ }
+
+ public static String[] array(String... keys) {
+ return toArray(get(keys).getAsJsonArray());
+ }
+
+ public static boolean bool(String... keys) {
+ return toBool(get(keys));
+ }
+
+ public static int integer(String... keys) {
+ return toInt(get(keys));
+ }
+
+ public static String string(String... keys) {
+ return toString(get(keys));
+ }
+
+ private static JsonElement get(String... keys) {
+ JsonElement obj = config;
+ for (String key : keys) {
+ JsonElement el = obj.getAsJsonObject().get(key);
+ if (el == null) {
+ if (create_flag) {
+ obj.getAsJsonObject().add(key, new JsonObject());
+ el = obj.getAsJsonObject().get(key);
+ } else {
+ return null;
+ }
+ }
+ obj = el;
+ }
+ return obj;
+ }
+
+ public static void add(String key, Object value, String... keys) {
+ create_flag = true;
+ JsonElement obj = get(keys);
+ if (obj != null && obj instanceof JsonObject) {
+ JsonObject prop = obj.getAsJsonObject();
+ if (value instanceof String) {
+ prop.addProperty(key, (String) value);
+ } else if (value instanceof Integer) {
+ prop.addProperty(key, (Integer) value);
+ } else if (value instanceof Boolean) {
+ prop.addProperty(key, (Boolean) value);
+ }
+ }
+ create_flag = false;
+ }
+
+ private static boolean toBool(JsonElement jsonElement) {
+ return (jsonElement == null ? false : jsonElement.getAsBoolean());
+ }
+
+ private static int toInt(JsonElement jsonElement) {
+ return (jsonElement == null ? 0 : jsonElement.getAsInt());
+ }
+
+ private static String toString(JsonElement jsonElement) {
+ return (jsonElement == null ? "" : jsonElement.getAsString());
+ }
+
+ private static String[] toArray(JsonArray jsonarray) {
+ String[] arr = new String[jsonarray.size()];
+ for (int i = 0; i < jsonarray.size(); i++) {
+ arr[i] = jsonarray.get(i).getAsString();
+ }
+ return arr;
+ }
+
+ private static int[] toIntArray(JsonArray jsonarray) {
+ int[] arr = new int[jsonarray.size()];
+ for (int i = 0; i < jsonarray.size(); i++) {
+ try {
+ arr[i] = Integer.parseInt(jsonarray.get(i).getAsString());
+ } catch (NumberFormatException e) {
+ arr[i] = 0;
+ }
+ }
+ return arr;
+ }
+}
diff --git a/src/com/proxy/kiwi/core/services/Folders.java b/src/com/proxy/kiwi/core/services/Folders.java
new file mode 100644
index 0000000..8d48f2a
--- /dev/null
+++ b/src/com/proxy/kiwi/core/services/Folders.java
@@ -0,0 +1,190 @@
+package com.proxy.kiwi.core.services;
+
+import com.proxy.kiwi.core.folder.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+
+public class Folders {
+ private static Folder root = null;
+
+ private static String currentFolder = "";
+
+ private static Path tempPath;
+
+ static {
+ tempPath = Paths.get(System.getProperty("java.io.tmpdir"), "Kiwi");
+ tempPath.toFile().mkdirs();
+ }
+
+ public static Thread thread;
+
+ public static Folder getRoot() {
+ return root;
+ }
+
+ public static String getCurrentFolderBuilt() {
+ return currentFolder;
+ }
+
+ public static Path getTempPath() {
+ return tempPath;
+ }
+
+ public static void buildDefaultRoot() {
+ thread = new Thread(() -> {
+ buildRoot(Config.getOption("path"));
+ });
+
+ thread.start();
+ }
+
+ public static Folder buildOnlyFromFile(String filepath) {
+ File file = new File(filepath);
+ switch (Type.getType(file)) {
+ case IMAGE:
+ file = file.getParentFile();
+ return new FileFolder(file.getName(), file.getAbsolutePath(), root);
+ case OTHER:
+ break;
+ case RAR:
+ return new RarFolder(file.getName(), file.getAbsolutePath(), root);
+ case SZ:
+ break;
+ case TAR:
+ break;
+ case ZIP:
+ return new ZipFolder(file.getName(), file.getAbsolutePath(), root);
+ case FOLDER:
+ return new FileFolder(file.getName(), file.getAbsolutePath(), root);
+ default:
+ break;
+
+ }
+ return null;
+ }
+
+ public static void buildRoot(String rootpath) {
+ File rootFile = new File(rootpath);
+
+ root = new FileFolder(rootFile.getName(), rootpath, null);
+ clean(root);
+ request(root);
+ }
+
+ public String getImagePath(Folder folder) {
+ if (Config.getFolderImage(folder.getName()) != null) {
+ return Config.getFolderImage(folder.getName());
+ } else {
+ return folder.getImagePath();
+ }
+ }
+
+ public static void print() {
+ print(root, 0);
+ }
+
+ private static void print(Folder folder, int depth) {
+ for (Folder child : folder.getSubfolders()) {
+ print(child, depth + 1);
+ }
+ }
+
+ public static void loadFolder(Folder folder) {
+ folder.load();
+ }
+
+ private static void request(Folder folder) {
+ for (Folder child : folder.getSubfolders()) {
+ Thumbnails.request(child);
+ }
+ for (Folder child : folder.getSubfolders()) {
+ request(child);
+ }
+ }
+
+ /**
+ * Removes any empty folders from the tree. Flattens folders with only one
+ * subfolder.
+ */
+ public static void clean(Folder folder) {
+ if (folder.hasSubfolders()) {
+ for (Folder child : folder.getSubfolders()) {
+ clean(child);
+ }
+ for (int i = 0; i < folder.getSubfolders().size(); i++) {
+ Folder child = folder.getSubfolders().get(i);
+ if (!child.hasSubfolders() && !child.hasVolumes()) {
+ folder.getSubfolders().remove(i);
+ } else if (child.getSubfolders().size() == 1) {
+ Folder f2 = child.getSubfolders().get(0);
+ folder.getSubfolders().remove(i);
+ folder.getSubfolders().add(i, f2);
+ f2.setParent(folder);
+ f2.getNameProperty().set(child.getName() + " - " + f2.getName());
+ }
+ }
+
+ }
+ }
+
+ public static void addToSize(Folder folder) {
+
+ }
+
+ /**
+ * Deletes the temporary directory made when extracting archives.
+ */
+ public static void deleteTemps() {
+
+ try {
+ Files.walkFileTree(tempPath, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
+ if (e == null) {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ } else {
+ // directory iteration failed
+ throw e;
+ }
+ }
+ });
+ } catch (FileSystemException e) {
+
+ } catch (IOException e) {
+ // TODO error message?
+ e.printStackTrace();
+ }
+
+ }
+
+ public static void uproot() {
+ Folder temp = root;
+ root = new FileFolder("root", "", null);
+ root.addFolder(temp);
+ }
+
+ public static int find(Folder folder, String filename) {
+ if (!folder.getIsLoadedProperty().get()) {
+ loadFolder(folder);
+ }
+
+ for (int i = 0; i < folder.getVolumes().size(); i++) {
+ Volume v = folder.getVolumes().get(i);
+ if (v.getFilename().equals(filename)) {
+ return i + 1;
+ }
+ }
+
+ return 1;
+ }
+}
diff --git a/src/com/proxy/kiwi/core/services/Instancer.java b/src/com/proxy/kiwi/core/services/Instancer.java
new file mode 100644
index 0000000..472584c
--- /dev/null
+++ b/src/com/proxy/kiwi/core/services/Instancer.java
@@ -0,0 +1,134 @@
+package com.proxy.kiwi.core.services;
+
+import com.proxy.kiwi.core.utils.Stopwatch;
+
+import java.io.*;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Paths;
+
+public abstract class Instancer {
+
+ /*
+ * search all files if there is a locked file (locked => program still open)
+ * go to the corresponding data file ( since program is waiting for input)
+ * write path exit else start normally
+ *
+ *
+ */
+
+ public static final String SELF_WAKE = "-1";
+
+ public abstract void pause(FileLock lock);
+
+ public abstract void resume(String input);
+
+ public abstract void shutdown();
+
+ protected abstract String getLockName();
+
+ protected abstract String getDataName();
+
+ public boolean sleep() throws IOException, OverlappingFileLockException {
+ int i = findFree();
+ if (i != -1) {
+
+ new Thread(() -> {
+ try {
+ File lock = getLockFile(i);
+
+ RandomAccessFile lockedFile = new RandomAccessFile(lock, "rw");
+ FileLock fileLock = lockedFile.getChannel().tryLock();
+
+ pause(fileLock);
+
+ File file = getDataFile(i);
+ if (file.exists()) {
+ file.delete();
+ }
+ file.createNewFile();
+ BufferedReader reader = new BufferedReader(new FileReader(file));
+ String input = null;
+ do {
+ Thread.sleep(50);
+ input = reader.readLine();
+ } while (input == null);
+
+ file.delete();
+
+ fileLock.release();
+ lockedFile.close();
+ reader.close();
+ resume(input);
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean wakeIfExists(String[] args) throws IOException {
+ Stopwatch.click("Checking for sleeping instances");
+ int i = findLock();
+ if (i != -1) {
+ File file = getDataFile(i);
+
+ try {
+ FileWriter w = new FileWriter(file);
+ w.write(args.length == 0 ? " " : args[0]);
+ w.close();
+ Stopwatch.click("Checking for sleeping instances");
+ return true;
+ } catch (IOException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ }
+ Stopwatch.click("Checking for sleeping instances");
+ return false;
+ }
+
+ public int findFree() throws IOException, OverlappingFileLockException {
+ for (int i = 0; i < 10; i++) {
+ File file = getLockFile(i);
+ RandomAccessFile lockedFile = new RandomAccessFile(file, "rw");
+ FileLock lock = lockedFile.getChannel().tryLock();
+ if (lock != null) {
+ lock.release();
+ lockedFile.close();
+ return i;
+ }
+ lockedFile.close();
+ }
+ return -1;
+ }
+
+ public int findLock() throws IOException {
+ for (int i = 0; i < 10; i++) {
+ File file = getLockFile(i);
+ RandomAccessFile lockedFile = new RandomAccessFile(file, "rw");
+ FileLock lock = lockedFile.getChannel().tryLock();
+ if (lock == null) {
+ lockedFile.close();
+ return i;
+ } else {
+ lock.release();
+ }
+ lockedFile.close();
+ }
+
+ return -1;
+ }
+
+ private File getDataFile(int i) {
+ return Paths.get(Folders.getTempPath().toString(), getDataName() + "-" + (i < 10 ? "0" + i : i)).toFile();
+ }
+
+ private File getLockFile(int i) {
+ return Paths.get(Folders.getTempPath().toString(), getLockName() + "-" + (i < 10 ? "0" + i : i)).toFile();
+ }
+}
diff --git a/src/com/proxy/kiwi/core/services/KiwiInstancer.java b/src/com/proxy/kiwi/core/services/KiwiInstancer.java
new file mode 100644
index 0000000..5109939
--- /dev/null
+++ b/src/com/proxy/kiwi/core/services/KiwiInstancer.java
@@ -0,0 +1,85 @@
+package com.proxy.kiwi.core.services;
+
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.utils.Log;
+import com.proxy.kiwi.core.utils.Stopwatch;
+import com.proxy.kiwi.reader.KiwiReadingPane;
+import javafx.application.Platform;
+import javafx.geometry.Dimension2D;
+import javafx.stage.Stage;
+
+import java.io.IOException;
+import java.nio.channels.FileLock;
+
+public class KiwiInstancer extends Instancer {
+
+ private static Stage stage;
+ private static FileLock lock;
+
+ public static void setStage(Stage stage) {
+ KiwiInstancer.stage = stage;
+ }
+
+ @Override
+ public void pause(FileLock lock) {
+ KiwiInstancer.lock = lock;
+ Log.print(Log.EVENT, "Sleeping!");
+ }
+
+ @Override
+ public void resume(String input) {
+ if (input.equals(Instancer.SELF_WAKE)) {
+ try {
+ lock.release();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ Log.print(Log.EVENT, "Woken up!\t\t(" + input + ")");
+ Stopwatch.click("Resuming");
+ Platform.runLater(() -> {
+
+ KiwiReadingPane pane = (KiwiReadingPane) stage.getScene().getRoot();
+
+ Dimension2D sizeDim = new Dimension2D(stage.getWidth(), stage.getHeight());
+ Dimension2D posDim = new Dimension2D(stage.getX(), stage.getY());
+
+ boolean wasFull = stage.isFullScreen();
+
+ Folder folder = Folder.fromFile(input);
+ if (folder != null) {
+ folder.load();
+
+ pane.setFolder(folder);
+ pane.changePage(Folders.find(folder, input));
+
+ }
+
+ stage.setWidth(sizeDim.getWidth());
+ stage.setHeight(sizeDim.getHeight());
+
+ stage.setX(posDim.getWidth());
+ stage.setY(posDim.getHeight());
+
+ stage.setFullScreen(wasFull);
+
+ stage.show();
+ Stopwatch.click("Resuming");
+ });
+ }
+
+ @Override
+ public void shutdown() {
+ System.exit(0);
+ }
+
+ @Override
+ protected String getLockName() {
+ return "kiwi.lock";
+ }
+
+ @Override
+ protected String getDataName() {
+ return "kiwi.data";
+ }
+}
diff --git a/src/com/proxy/kiwi/core/services/Thumbnails.java b/src/com/proxy/kiwi/core/services/Thumbnails.java
new file mode 100644
index 0000000..5d09d08
--- /dev/null
+++ b/src/com/proxy/kiwi/core/services/Thumbnails.java
@@ -0,0 +1,147 @@
+package com.proxy.kiwi.core.services;
+
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.image.Cache;
+import com.proxy.kiwi.core.image.KiwiImage;
+import com.proxy.kiwi.core.utils.Log;
+import com.proxy.kiwi.core.utils.Resources;
+import javafx.application.Platform;
+import javafx.scene.image.Image;
+
+import java.io.File;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class Thumbnails {
+ private static Image loading;
+ private static Cache thumbnailCache = new Cache(2000);
+
+ public static int thumb_width, thumb_height;
+
+ private static LinkedBlockingQueue requestQueue = new LinkedBlockingQueue<>();
+ private static LinkedBlockingQueue expressQueue = new LinkedBlockingQueue<>();
+
+ private static ThumbnailThread thread;
+ static volatile boolean alive;
+
+ public static void init() {
+ alive = true;
+ thumb_width = Config.getIntOption("item_width");
+ thumb_height = Config.getIntOption("item_height");
+ thread = new ThumbnailThread();
+ thread.start();
+
+ Platform.runLater(() -> {
+ loading = new Image(Resources.get("loading.gif").toString());
+ });
+
+ }
+
+ public static void stop() {
+ alive = false;
+ }
+
+ @Deprecated
+ public static void pause() {
+ thread.suspend();
+ }
+
+ @Deprecated
+ public static void resume() {
+ thread.resume();
+ }
+
+ public static Image getLoading() {
+ return loading;
+ }
+
+ public static Cache getCache() {
+ return thumbnailCache;
+ }
+
+ public synchronized static LinkedBlockingQueue getRequests() {
+ return requestQueue;
+ }
+
+ public synchronized static LinkedBlockingQueue getExpress() {
+ return expressQueue;
+ }
+
+ public static void request(Folder folder) {
+ requestQueue.add(folder);
+ }
+
+ public static void requestExpress(Folder folder) {
+ expressQueue.add(folder);
+ }
+
+ public static void requestExpressOverwrite(Folder folder) {
+ expressQueue.clear();
+ for (Folder child : folder.getSubfolders()) {
+ expressQueue.add(child);
+ }
+ }
+}
+
+class ThumbnailThread extends Thread {
+ public void run() {
+
+ Folder folder;
+
+ while ((Thumbnails.alive)) {
+ try {
+ folder = Thumbnails.getExpress().poll(100, TimeUnit.MILLISECONDS);
+ if (folder != null) {
+ process(folder, true);
+ continue;
+ }
+ // folder =
+ // Thumbnails.getRequests().poll(100,TimeUnit.MILLISECONDS);
+ // if (folder != null) { process(folder,false); }
+ } catch (InterruptedException e) {
+ Log.print(e);
+ }
+ }
+ }
+
+ public void process(Folder folder, boolean backgroundLoad) {
+ String filename = folder.getImagePath();
+
+ if (filename != null && !Thumbnails.getCache().contains(filename)) {
+
+ int targetW = Thumbnails.thumb_width;
+ int targetH = Thumbnails.thumb_height;
+
+ KiwiImage image = null;
+
+ double canvasRatio = targetW / (1.0 * targetH);
+
+ /**
+ * Create a dummy version of the file to check the proportions. Then
+ * use the target ratio to load the correct sized image.
+ */
+ Image dummy = new Image("file:" + filename, targetW, targetH, true, false);
+
+ double w = dummy.getWidth();
+ double h = dummy.getHeight();
+
+ double scalingRatio;
+
+ if (w / h > canvasRatio) {
+ /* Crop width, constrain height */
+ scalingRatio = targetH / h;
+ } else {
+ /* Crop height, constrain width */
+ scalingRatio = targetW / w;
+ }
+
+ image = new KiwiImage(new File(filename), (int) (scalingRatio * w), (int) (scalingRatio * h), true, true,
+ backgroundLoad);
+
+ Thumbnails.getCache().add(filename, image);
+
+ dummy = null;
+
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/core/utils/Command.java b/src/com/proxy/kiwi/core/utils/Command.java
new file mode 100644
index 0000000..8536738
--- /dev/null
+++ b/src/com/proxy/kiwi/core/utils/Command.java
@@ -0,0 +1,53 @@
+package com.proxy.kiwi.core.utils;
+
+import javafx.scene.input.KeyCode;
+
+public enum Command {
+
+ UP("Up",KeyCode.UP,KeyCode.W),
+ DOWN("Down",KeyCode.DOWN,KeyCode.S),
+ LEFT("Left",KeyCode.LEFT,KeyCode.A),
+ RIGHT("Right",KeyCode.RIGHT,KeyCode.D),
+ ENTER("Enter",KeyCode.ENTER),
+ BACK("Back",KeyCode.BACK_SPACE),
+ FULL_SCREEN("Full Screen",KeyCode.F),
+ MINIMIZE("Minimize",KeyCode.M),
+ EXIT("Exit",KeyCode.X),
+ ZOOM_IN("Zoom In",KeyCode.EQUALS),
+ ZOOM_OUT("Zoom Out",KeyCode.MINUS),
+ CHAPTER_NEXT("Next Chapter",KeyCode.E),
+ CHAPTER_PREVIOUS("Previous Chapter",KeyCode.Q),
+ CHAPTER_ADD("Add Chapter",KeyCode.COMMA),
+ CHAPTER_REMOVE("Remove Chapter",KeyCode.PERIOD),
+ NEXT_FOLDER("Next Folder", KeyCode.L),
+ PREVIOUS_FOLDER("Previous Folder", KeyCode.K),
+ QUALITY("Change Quality", KeyCode.B),
+ OPTIONS("Options", KeyCode.ALT),
+
+ UNDEFINED("Undefined");
+
+ public final String[] default_hotkeys;
+ final String name;
+
+ private Command(String name, KeyCode... hotkeys) {
+ this.name = name;
+ default_hotkeys = new String[hotkeys.length];
+ for (int i = 0; i < hotkeys.length; i++) {
+ default_hotkeys[i] = hotkeys[i].getName();
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public static Command get(String name) {
+ for (Command command : values()) {
+ if (command.getName().equals(name)) {
+ return command;
+ }
+ }
+
+ return Command.UNDEFINED;
+ }
+}
diff --git a/src/com/proxy/kiwi/core/utils/FXTools.java b/src/com/proxy/kiwi/core/utils/FXTools.java
new file mode 100644
index 0000000..18ab446
--- /dev/null
+++ b/src/com/proxy/kiwi/core/utils/FXTools.java
@@ -0,0 +1,42 @@
+package com.proxy.kiwi.core.utils;
+
+import java.util.concurrent.CountDownLatch;
+
+import javafx.application.Platform;
+
+public class FXTools {
+
+ /**
+ * Runs the specified {@link Runnable} on the
+ * JavaFX application thread and waits for completion.
+ *
+ * @param action the {@link Runnable} to run
+ * @throws NullPointerException if {@code action} is {@code null}
+ */
+ public static void runAndWait(Runnable action) {
+ if (action == null)
+ throw new NullPointerException("action");
+
+ // run synchronously on JavaFX thread
+ if (Platform.isFxApplicationThread()) {
+ action.run();
+ return;
+ }
+
+ // queue on JavaFX thread and wait for completion
+ final CountDownLatch doneLatch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ try {
+ action.run();
+ } finally {
+ doneLatch.countDown();
+ }
+ });
+
+ try {
+ doneLatch.await();
+ } catch (InterruptedException e) {
+ // ignore exception
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/core/utils/FileListener.java b/src/com/proxy/kiwi/core/utils/FileListener.java
new file mode 100644
index 0000000..cc9677a
--- /dev/null
+++ b/src/com/proxy/kiwi/core/utils/FileListener.java
@@ -0,0 +1,33 @@
+package com.proxy.kiwi.core.utils;
+
+import java.io.File;
+
+public class FileListener {
+
+ private long lastModified;
+
+ public static FileListener create(String path, Runnable onChange) {
+ return new FileListener(path, onChange);
+ }
+
+ public FileListener(String path, Runnable onChange) {
+ this.lastModified = new File(path).lastModified();
+
+ new Thread( ()-> {
+ while (true) {
+ try {
+ Thread.sleep(1000);
+ File file = new File(path);
+ long newTime = file.lastModified();
+ if (newTime > lastModified) {
+ onChange.run();
+ this.lastModified = newTime;
+ }
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }).start();
+ }
+}
diff --git a/src/com/proxy/kiwi/core/utils/Log.java b/src/com/proxy/kiwi/core/utils/Log.java
new file mode 100644
index 0000000..c20b856
--- /dev/null
+++ b/src/com/proxy/kiwi/core/utils/Log.java
@@ -0,0 +1,121 @@
+package com.proxy.kiwi.core.utils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+public enum Log {
+ TIME("TIME"), GUI("GUI ", false), EVENT("EVNT"), IO("I /O"), PRELOAD("LOAD", false), ERR("ERR ");
+
+ private final String string;
+ private final boolean visible;
+ private static JFrame logwindow;
+ private static TextArea log;
+ private static final boolean DEBUG = false;
+ private static final int MAX_STACK_DEPTH = 10;
+
+ Log(String s) {
+ this(s, true);
+ }
+
+ Log(String s, boolean v) {
+ string = s;
+ visible = v;
+ }
+
+ public String toString() {
+ return string;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public static void print(Log type, String message) {
+ if (type.isVisible()) {
+ String output = "[" + type + "]\t" + message;
+
+ System.out.println(output);
+
+ if (DEBUG) {
+ if (logwindow == null) {
+ initWindow();
+ }
+ if (log != null) {
+ log.setText(log.getText() + output + "\n");
+ }
+ }
+ }
+ }
+
+ public static void print(Exception e) {
+ print(Log.ERR, e.getClass().getCanonicalName() + " on Thread " + Thread.currentThread().getName());
+ if (e.getMessage() != null) {
+ print(Log.ERR, e.getMessage());
+ }
+ for (int i = 0; i < MAX_STACK_DEPTH && i < e.getStackTrace().length; i++) {
+ StackTraceElement el = e.getStackTrace()[i];
+ print(Log.ERR, "\t...at " + el.getClassName() + "." + el.getMethodName()
+ + (el.getFileName() != null ? (" (" + el.getFileName() + ":" + el.getLineNumber()) + ")" : ""));
+
+ }
+ }
+
+ public static void alert(Exception e) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(e.getMessage());
+ for (int i = 0; i < MAX_STACK_DEPTH && i < e.getStackTrace().length; i++) {
+ StackTraceElement el = e.getStackTrace()[i];
+ sb.append("\n\t...at " + el.getClassName() + "." + el.getMethodName()
+ + (el.getFileName() != null ? (" (" + el.getFileName() + ":" + el.getLineNumber()) + ")" : ""));
+ }
+
+ JOptionPane.showConfirmDialog(null, sb.toString());
+ }
+
+ private static void initWindow() {
+ logwindow = new JFrame();
+ Container pane = logwindow.getContentPane();
+ if (log == null) {
+ log = new TextArea();
+ }
+ pane.add(log);
+ logwindow.setSize(600, 600);
+ logwindow.setVisible(true);
+ logwindow.addWindowListener(new LogWindowListener());
+
+ }
+
+ static class LogWindowListener implements WindowListener {
+
+ @Override
+ public void windowOpened(WindowEvent e) {
+ }
+
+ @Override
+ public void windowIconified(WindowEvent e) {
+ }
+
+ @Override
+ public void windowDeiconified(WindowEvent e) {
+ }
+
+ @Override
+ public void windowDeactivated(WindowEvent e) {
+ }
+
+ @Override
+ public void windowClosing(WindowEvent e) {
+ }
+
+ @Override
+ public void windowActivated(WindowEvent e) {
+ }
+
+ @Override
+ public void windowClosed(WindowEvent e) {
+ logwindow = null;
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/core/utils/Resources.java b/src/com/proxy/kiwi/core/utils/Resources.java
new file mode 100644
index 0000000..324d0ba
--- /dev/null
+++ b/src/com/proxy/kiwi/core/utils/Resources.java
@@ -0,0 +1,67 @@
+package com.proxy.kiwi.core.utils;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.*;
+import java.net.URL;
+import java.util.ArrayList;
+
+/**
+ * Helper class for reading compiled files. The compiled files resides inside
+ * the src directory.
+ *
+ * @author Daniel
+ *
+ */
+public class Resources {
+ public static final String RES_FOLDER = "com/proxy/kiwi/res/";
+
+ public static URL get(String path) {
+
+ return Resources.class.getClassLoader().getResource(RES_FOLDER + path);
+ }
+
+ public static ArrayList getAll(String... paths) {
+ ArrayList strings = new ArrayList<>();
+ for (String path : paths) {
+ strings.add(Resources.get(path).toExternalForm());
+ }
+ return strings;
+ }
+
+ public static File getFile(String path, String name) {
+ try {
+ File temp = File.createTempFile(name, ".kiwitemp");
+ temp.deleteOnExit();
+ InputStream in = get(path).openStream();
+ FileOutputStream out = new FileOutputStream(temp);
+ IOUtils.copy(in, out);
+ out.close();
+ in.close();
+
+ return temp;
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static String getContent(String path) {
+ URL url = Resources.class.getClassLoader().getResource(RES_FOLDER + path);
+ StringBuilder out = new StringBuilder();
+
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ out.append(line);
+ }
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ return out.toString();
+ }
+}
diff --git a/src/com/proxy/kiwi/core/utils/Stopwatch.java b/src/com/proxy/kiwi/core/utils/Stopwatch.java
new file mode 100644
index 0000000..4642fb8
--- /dev/null
+++ b/src/com/proxy/kiwi/core/utils/Stopwatch.java
@@ -0,0 +1,16 @@
+package com.proxy.kiwi.core.utils;
+
+import java.util.HashMap;
+
+public class Stopwatch {
+ private static HashMap timers = new HashMap<>();
+
+ public static void click(String s) {
+ if (timers.containsKey(s)) {
+ Log.print(Log.TIME, s + " took " + (System.currentTimeMillis() - timers.remove(s)) / 1000.0 + " seconds.");
+ } else {
+ timers.put(s, System.currentTimeMillis());
+ Log.print(Log.TIME, s + "...");
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/explorer/EditController.java b/src/com/proxy/kiwi/explorer/EditController.java
new file mode 100644
index 0000000..052466c
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/EditController.java
@@ -0,0 +1,107 @@
+package com.proxy.kiwi.explorer;
+
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.Thumbnails;
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.FlowPane;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+public class EditController {
+ @FXML private TextField tagInput;
+ @FXML private TextField artistInput;
+
+ @FXML private ChoiceBox tagChoice;
+ @FXML private ChoiceBox artistChoice;
+
+ @FXML private FlowPane tagPane;
+ @FXML private FlowPane artistPane;
+
+ @FXML private ImageView imageView;
+
+ Folder folder;
+
+ public void init(Folder folder) {
+ this.folder = folder;
+
+ Set allTags = Config.getTags();
+ // Set allArtists = Settings.getArtists();
+
+ Set tags = Config.getTags(folder);
+ // Set artists = Settings.getArtists(folder);
+
+ imageView.setImage(Thumbnails.getCache().get(folder.getImagePath()));
+
+ System.out.println(tags.size());
+ for (String tag : tags) {
+ addTagToPane(tag);
+ }
+
+ tagChoice.getItems().addAll(allTags);
+
+ tagChoice.setOnAction((e) -> {
+ addChoiceTag();
+ });
+ }
+
+ @FXML
+ public void addTag() {
+ String tag = tagInput.getText();
+ addTagToPane(tag);
+ }
+
+ @FXML
+ public void addChoiceTag() {
+ String tag = tagChoice.getValue();
+ addTagToPane(tag);
+ }
+
+ @FXML
+ public void save() {
+ Config.setTags(folder, getTags());
+ }
+
+ @FXML
+ public void cancel() {
+ EditMenu.get().hide();
+ }
+
+ private void addTagToPane(String tag) {
+ tag = tag.toLowerCase().trim();
+ for (Node node : tagPane.getChildren()) {
+ Label label = (Label) (node);
+ if (label.getText().equals(tag)) {
+ return;
+ }
+ }
+ Label label = new Label(tag);
+ label.setOnMouseClicked((e) -> {
+ tagPane.getChildren().remove(label);
+ });
+ tagPane.getChildren().add(label);
+ }
+
+ private Set getTags() {
+ Set tags = new HashSet<>();
+ tagPane.getChildren().forEach((node) -> {
+ tags.add(((Label) (node)).getText());
+ });
+ return tags;
+ }
+
+ // private Set getArtists() {
+ // Set artists = new HashSet<>();
+ // artistPane.getChildren().forEach( (node) -> {
+ // artists.add( ((Label)(node)).getText());
+ // });
+ // return artists;
+ // }
+}
diff --git a/src/com/proxy/kiwi/explorer/EditMenu.java b/src/com/proxy/kiwi/explorer/EditMenu.java
new file mode 100644
index 0000000..da5662f
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/EditMenu.java
@@ -0,0 +1,45 @@
+package com.proxy.kiwi.explorer;
+
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.utils.Resources;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Stage;
+
+import java.io.IOException;
+
+public class EditMenu extends Stage {
+ static EditMenu instance;
+
+ public static EditMenu get() {
+ return instance;
+ }
+
+ public static void show(Folder folder) {
+ if (instance != null) {
+ instance.hide();
+ }
+ instance = new EditMenu(folder);
+ instance.show();
+
+ }
+
+ Folder folder;
+
+ public EditMenu(Folder folder) {
+ this.folder = folder;
+ try {
+ FXMLLoader loader = new FXMLLoader(Resources.get("editmenu.fxml"));
+ GridPane pane = loader.load();
+ EditController controller = loader.getController();
+ controller.init(folder);
+ setScene(new Scene(pane, 900, 600));
+
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+}
diff --git a/src/com/proxy/kiwi/explorer/FadeNode.java b/src/com/proxy/kiwi/explorer/FadeNode.java
new file mode 100644
index 0000000..b23a029
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/FadeNode.java
@@ -0,0 +1,23 @@
+package com.proxy.kiwi.explorer;
+
+import javafx.animation.Transition;
+import javafx.scene.Node;
+import javafx.util.Duration;
+
+class FadeNode extends Transition {
+
+ Node node;
+ boolean hide;
+
+ public FadeNode(Node node, boolean hide) {
+ this.node = node;
+ this.hide = hide;
+ setCycleDuration(Duration.millis(200));
+ }
+
+ @Override
+ protected void interpolate(double frac) {
+ node.setOpacity(hide ? 1 - frac : frac);
+ }
+
+}
diff --git a/src/com/proxy/kiwi/explorer/FolderMenu.java b/src/com/proxy/kiwi/explorer/FolderMenu.java
new file mode 100644
index 0000000..92aa027
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/FolderMenu.java
@@ -0,0 +1,97 @@
+package com.proxy.kiwi.explorer;
+
+import javafx.scene.control.*;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import com.proxy.kiwi.core.services.Config;
+
+public class FolderMenu extends ContextMenu {
+
+ FolderPanel panel;
+ Menu tagMenu;
+
+ public FolderMenu(FolderPanel panel) {
+ this.panel = panel;
+
+ if (!panel.folder.hasSubfolders()) {
+ tagMenu = new Menu("Tags");
+
+ Set allTags = Config.getTags();
+
+ Set tags = Config.getTags(panel.folder);
+
+ for (String tag : allTags) {
+ CheckMenuItem item = new CheckMenuItem(tag);
+ if (tags.contains(tag)) {
+ item.setSelected(true);
+ }
+ item.setOnAction((e) -> {
+ updateTags();
+ });
+ tagMenu.getItems().add(item);
+ }
+
+ getItems().add(tagMenu);
+
+ MenuItem newTag = new MenuItem("Add new tag...");
+
+ newTag.setOnAction((e) -> {
+ addTag();
+ });
+
+ getItems().add(newTag);
+ }
+
+ MenuItem open = new MenuItem("Open in Explorer");
+ open.setOnAction((e) -> {
+ open();
+ });
+ getItems().add(open);
+
+ }
+
+ public void addTag() {
+ TextInputDialog dialog = new TextInputDialog();
+ dialog.setHeaderText("Add new tag");
+ Optional res = dialog.showAndWait();
+ if (res.isPresent() && !res.get().trim().equals(" ")) {
+ CheckMenuItem item = new CheckMenuItem(res.get().trim());
+ item.setSelected(true);
+ tagMenu.getItems().add(item);
+ updateTags();
+ }
+ }
+
+ public void updateTags() {
+ Set tags = new HashSet<>();
+ for (MenuItem item : tagMenu.getItems()) {
+ CheckMenuItem check = (CheckMenuItem) (item);
+ if (check.isSelected()) {
+ tags.add(check.getText());
+ }
+ }
+
+ Config.setTags(panel.folder, tags);
+ }
+
+ public void open() {
+ try {
+ Desktop.getDesktop().open(new File(panel.folder.getFilenameProperty().get()));
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ public void editTags() {
+ EditMenu.show(panel.folder);
+ }
+}
diff --git a/src/com/proxy/kiwi/explorer/FolderPanel.java b/src/com/proxy/kiwi/explorer/FolderPanel.java
new file mode 100644
index 0000000..cc35de1
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/FolderPanel.java
@@ -0,0 +1,185 @@
+package com.proxy.kiwi.explorer;
+
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.Thumbnails;
+import com.proxy.kiwi.core.utils.Log;
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+import javafx.util.Duration;
+
+public class FolderPanel extends StackPane {
+
+ public Label name;
+ public ImageView view;
+ public Folder folder;
+ public Timeline timeline;
+
+ final Animation hide, show;
+
+ private boolean isAnimating = false;
+
+ boolean hidden;
+ boolean infoHidden;
+ public int xOff, yOff;
+ private FadeNode showInfo;
+ private FadeNode hideInfo;
+
+ private VBox vbox;
+
+ public FolderPanel(Folder folder, KiwiExplorerPane pane) {
+ setMinSize(Thumbnails.thumb_width, Thumbnails.thumb_height);
+ this.folder = folder;
+ view = new ImageView();
+
+ view.setImage(Thumbnails.getLoading());
+ getChildren().add(view);
+
+ vbox = new VBox();
+ vbox.setBackground(
+ new Background(new BackgroundFill(new Color(0.0, 0.0, 0.0, 0.45), new CornerRadii(0), new Insets(0))));
+ StackPane.setAlignment(vbox, Pos.BOTTOM_CENTER);
+
+ vbox.setPrefSize(Thumbnails.thumb_width, 0.2 * Thumbnails.thumb_height);
+ vbox.setMaxSize(Thumbnails.thumb_width, 0.2 * Thumbnails.thumb_height);
+
+ name = new Label(folder.getName());
+ name.setPrefWidth(Thumbnails.thumb_width);
+ name.setPrefHeight(0.2 * Thumbnails.thumb_height);
+ name.getStyleClass().addAll("text", "folder-title");
+
+ vbox.getChildren().add(name);
+
+ showInfo = new FadeNode(vbox, false);
+
+ hideInfo = new FadeNode(vbox, true);
+
+ getChildren().add(vbox);
+ vbox.setOpacity(0.0);
+ infoHidden = true;
+
+ hidden = true;
+ setOpacity(0.0);
+ setDisable(true);
+ setPrefSize(0, 0);
+ setMaxSize(0, 0);
+ setMinSize(0, 0);
+
+ show = new FadeNode(this, false);
+
+ hide = new FadeNode(this, true);
+
+ hide.setOnFinished((event) -> {
+ setPrefSize(0, 0);
+ setMaxSize(0, 0);
+ setMinSize(0, 0);
+ isAnimating = false;
+ });
+
+ show.setOnFinished((event) -> {
+ isAnimating = false;
+ });
+
+ setOnMouseClicked((event) -> {
+ pane.handlePanelClick(event, folder);
+ });
+
+ setOnMouseEntered((event) -> {
+ if (pane.selected != null) {
+ pane.selected.deselect();
+ }
+ pane.setSelected(this);
+ if (!hidden) {
+ select();
+ }
+ });
+
+ setOnMouseExited((event) -> {
+ if (!hidden) {
+ deselect();
+ }
+ });
+
+ timeline = new Timeline(new KeyFrame(Duration.seconds(0.1), (event) -> {
+ if (Thumbnails.getCache().contains(folder.getImagePath())) {
+ Image image = Thumbnails.getCache().get(folder.getImagePath());
+ if (image.getProgress() == 1) {
+ setImage(image);
+ } else {
+ image.progressProperty().addListener((ov, old_val, new_val) -> {
+ new_val = new_val.doubleValue() * 100.0;
+ if (new_val.doubleValue() == 100.0) {
+ Log.print(Log.GUI, "Setting image for " + folder.getName());
+ setImage(image);
+ }
+ });
+ }
+
+ timeline.stop();
+ }
+ }));
+
+ timeline.setCycleCount(Timeline.INDEFINITE);
+ timeline.play();
+
+ }
+
+ private void setImage(Image image) {
+ view.setImage(image);
+
+ xOff = Config.getFolderXOffset(folder.getName());
+ yOff = Config.getFolderYOffset(folder.getName());
+
+ Rectangle2D viewport = new Rectangle2D(xOff, yOff, Thumbnails.thumb_width, Thumbnails.thumb_height);
+ view.setViewport(viewport);
+ view.setPreserveRatio(true);
+ view.setSmooth(true);
+ }
+
+ public void setHidden(boolean hidden) {
+ if (hidden && !this.hidden) {
+ if (!isAnimating) {
+ isAnimating = true;
+ hide.play();
+ }
+ setDisable(true);
+ this.hidden = true;
+ } else if (!hidden && this.hidden) {
+ setPrefSize(Thumbnails.thumb_width, Thumbnails.thumb_height);
+ setMaxSize(Thumbnails.thumb_width, Thumbnails.thumb_height);
+ setMinSize(Thumbnails.thumb_width, Thumbnails.thumb_height);
+ if (!isAnimating) {
+ isAnimating = true;
+ show.play();
+ }
+ setDisable(false);
+ this.hidden = false;
+ }
+ }
+
+ public void select() {
+ if (!hidden && infoHidden) {
+ showInfo.play();
+ infoHidden = false;
+ }
+
+ }
+
+ public void deselect() {
+ if (!hidden && !infoHidden) {
+ hideInfo.play();
+ infoHidden = true;
+ } else {
+ vbox.setOpacity(0);
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/explorer/KiwiExplorerPane.java b/src/com/proxy/kiwi/explorer/KiwiExplorerPane.java
new file mode 100644
index 0000000..bb9e499
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/KiwiExplorerPane.java
@@ -0,0 +1,462 @@
+package com.proxy.kiwi.explorer;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import com.proxy.kiwi.app.KiwiApplication;
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.Folders;
+import com.proxy.kiwi.core.services.Thumbnails;
+import com.proxy.kiwi.core.utils.Resources;
+import com.proxy.kiwi.core.utils.Stopwatch;
+
+import javafx.animation.TranslateTransition;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.geometry.Bounds;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+import javafx.stage.DirectoryChooser;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+
+public class KiwiExplorerPane extends AnchorPane{
+
+ static final String FXML_FILE = "explorer_pane.fxml";
+
+ @FXML private SearchBox searchBox;
+
+ @FXML private ScrollPane scrollPane;
+ @FXML private FlowPane flowPane;
+ @FXML private CheckBox collapseCheck;
+
+ @FXML private Button optionsButton;
+
+ @FXML private ScrollPane menu;
+
+ @FXML private Label libraryButton;
+ @FXML private VBox libraryMenu;
+
+ @FXML private Label hotkeyButton;
+ @FXML private VBox hotkeyMenu;
+
+ @FXML public VBox loadingBox;
+ @FXML public ProgressBar loadingBar;
+ @FXML public Label loadingLabel;
+
+ public FolderPanel selected;
+
+ boolean showMenu = false;
+ TranslateTransition menuAnimation;
+
+ LinkedBlockingQueue folderQueue;
+
+ public double scrollSpeed;
+
+ public FolderMenu contextMenu;
+
+ private Stage stage;
+
+ public KiwiExplorerPane(Stage stage, String path) {
+
+ this.stage = stage;
+
+ loadLayout();
+ init();
+ }
+
+ public void init() {
+ stage.setTitle("Kiwi");
+ Folders.buildRoot(Config.getOption("path"));
+
+ folderQueue = new LinkedBlockingQueue();
+
+ menuAnimation = new TranslateTransition(Duration.millis(250), menu);
+
+ setMenuParent(libraryButton, libraryMenu);
+
+ setMenuParent(hotkeyButton, hotkeyMenu);
+
+ searchBox.getQuery().addListener((obs, old, n) -> updateView());
+
+ searchBox.onChange(this::updateView);
+
+ searchBox.onSingleInteract( () -> {
+ FolderPanel panel = getSingleVisible();
+ if (panel != null) {
+ interact(panel.folder);
+ }
+ });
+
+ searchBox.onEmptyFocus(flowPane);
+
+ resetLibraries();
+
+ Config.getHotkeys().forEach((entry) -> {
+ HBox hbox = new HBox();
+ Label name = new Label(entry.getKey());
+ name.getStyleClass().add("sub-menu-item");
+ Label val = new Label(entry.getValue().getAsJsonArray().toString());
+ val.getStyleClass().add("sub-menu-item");
+
+ hbox.getChildren().addAll(name, val);
+ hotkeyMenu.getChildren().add(hbox);
+ });
+
+ Platform.runLater(() -> {
+ try {
+ if (Folders.thread != null) {
+ Folders.thread.join();
+ }
+
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ addPanels(Folders.getRoot());
+ updateView();
+
+ if (!flowPane.getChildren().isEmpty()) {
+ selected = (FolderPanel) flowPane.getChildren().get(0);
+ selected.select();
+ }
+ });
+
+ scrollSpeed = Config.getIntOption("item_height") / 3.0;
+ }
+
+ public void toggleSubmenu(Pane submenu) {
+ if (submenu.isManaged()) {
+ submenu.setMaxHeight(0);
+ submenu.setOpacity(0);
+ submenu.setManaged(false);
+ } else {
+ submenu.setMaxHeight(libraryButton.getHeight());
+ submenu.setOpacity(1);
+ submenu.setManaged(true);
+ }
+ }
+
+ @FXML
+ public void handleKeyEvent(KeyEvent event) {
+ if (contextMenu != null && contextMenu.isShowing()) {
+ event.consume();
+ return;
+ }
+
+ int size = flowPane.getChildren().size();
+ List children = flowPane.getChildren();
+
+ switch (Config.getCommandFor(event.getCode())) {
+ case ENTER:
+ interact(selected.folder);
+ event.consume();
+ break;
+ case MINIMIZE:
+ stage.setIconified(true);
+ break;
+ case FULL_SCREEN:
+ stage.setFullScreen(!stage.isFullScreen());
+ break;
+ case EXIT:
+ System.exit(0);
+ break;
+ case BACK:
+ searchBox.signalBack();
+ event.consume();
+ break;
+ case LEFT:
+ for (int i = size - 1; i >= 0; i--) {
+ if (children.get(i) == selected) {
+ for (int j = 1; j < size; j++) {
+ FolderPanel panel = (FolderPanel) children.get((i - j + size) % size);
+ if (!panel.hidden) {
+ selected.deselect();
+ selected = panel;
+ selected.select();
+ checkInBounds();
+ return;
+ }
+ }
+ }
+ }
+ break;
+ case RIGHT:
+ for (int i = 0; i < size; i++) {
+ if (children.get(i) == selected) {
+ for (int j = 1; j < size; j++) {
+ FolderPanel panel = (FolderPanel) children.get((i + j) % size);
+ if (!panel.hidden) {
+ selected.deselect();
+ selected = panel;
+ selected.select();
+ checkInBounds();
+ return;
+ }
+ }
+ }
+ }
+ break;
+ case UP:
+ for (int i = size - 1; i >= 0; i--) {
+ if (children.get(i) == selected) {
+ for (int j = 1; j < size; j++) {
+ FolderPanel panel = (FolderPanel) children.get((i - j + size) % size);
+ if (!panel.hidden && selected.getLayoutX() == panel.getLayoutX()) {
+ selected.deselect();
+ selected = panel;
+ selected.select();
+ event.consume();
+ checkInBounds();
+ return;
+ }
+ }
+ }
+ }
+ break;
+ case DOWN:
+ for (int i = 0; i < size; i++) {
+ if (children.get(i) == selected) {
+ for (int j = 1; j < size; j++) {
+ FolderPanel panel = (FolderPanel) children.get((i + j) % size);
+ if (!panel.hidden && selected.getLayoutX() == panel.getLayoutX()) {
+ selected.deselect();
+ selected = panel;
+ selected.select();
+ event.consume();
+ checkInBounds();
+ return;
+ }
+ }
+ }
+ }
+ break;
+ case OPTIONS:
+ if (selected == null) {
+ break;
+ }
+ Bounds b = selected.localToScene(selected.getBoundsInLocal());
+ int x = (int) ( 0.5*(b.getMinX() + b.getMaxX()) + selected.getScene().getWindow().getX() );
+ int y= (int) ( 0.5*(b.getMinY() + b.getMaxY()) + selected.getScene().getWindow().getY() );
+ showContextMenu(selected.folder, x, y);
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ @FXML
+ public void updateView() {
+ flowPane.getChildren().forEach((comp) -> {
+ FolderPanel child = (FolderPanel) (comp);
+ updateChild(child);
+ });
+ flowPane.autosize();
+ }
+
+ public void updateLast() {
+ FolderPanel lastChild = (FolderPanel) (flowPane.getChildren().get(flowPane.getChildren().size() - 1));
+ updateChild(lastChild);
+ flowPane.autosize();
+ }
+
+ private void updateChild(FolderPanel child) {
+ if (searchBox.accept(child, collapseCheck.isSelected())) {
+ child.setHidden(false);
+
+ Thumbnails.requestExpress(child.folder);
+ } else {
+ child.setHidden(true);
+ }
+ }
+
+ @FXML
+ public void showMenu(MouseEvent event) {
+ if (showMenu) {
+ menuAnimation.setFromX(0);
+ menuAnimation.setToX(menu.getWidth());
+ } else {
+ menuAnimation.setFromX(menu.getWidth());
+ menuAnimation.setToX(0);
+ }
+ showMenu = !showMenu;
+ menuAnimation.play();
+ }
+
+ public void handlePanelClick(MouseEvent event, Folder folder) {
+ if (event.getButton().equals(MouseButton.PRIMARY)) {
+ interact(folder);
+ } else if (event.getButton().equals(MouseButton.SECONDARY)) {
+ showContextMenu(folder, (int) event.getScreenX(), (int) event.getScreenY());
+ }
+ }
+
+ public void showContextMenu(Folder folder, int x, int y) {
+ if (contextMenu != null) {
+ contextMenu.hide();
+ }
+ contextMenu = new FolderMenu(selected);
+ contextMenu.show(selected, x, y);
+ }
+
+ public void selectLibrary(String path) {
+ Config.setOption("path", path);
+
+ init();
+
+ }
+
+ @FXML
+ public void changeLibrary() {
+
+ DirectoryChooser dc = new DirectoryChooser();
+ dc.setTitle("Select Library location");
+ dc.setInitialDirectory(new File(Config.getOption("path")));
+
+ File selected = dc.showDialog(stage.getOwner());
+ if (selected != null) {
+ Config.setOption("path", selected.getAbsolutePath());
+ Config.addLibrary(selected.getAbsolutePath());
+ Folders.buildRoot(Config.getOption("path"));
+ init();
+ }
+ }
+
+ @FXML
+ public void scroll(ScrollEvent event) {
+ double v = scrollPane.getVvalue();
+ double percent = scrollSpeed / (flowPane.getHeight() - scrollPane.getHeight());
+
+ scrollPane.setVvalue(v + (event.getDeltaY() > 0 ? -(percent) : percent));
+ event.consume();
+ }
+
+ public void setSelected(FolderPanel panel) {
+ if (selected != null) {
+ selected.deselect();
+ }
+ this.selected = panel;
+ }
+
+ private void resetLibraries() {
+ for (Iterator iterator = libraryMenu.getChildren().iterator(); iterator.hasNext();) {
+ Label label = (Label) (iterator.next());
+ if (label.getText().contains("/")) {
+ iterator.remove();
+ }
+ }
+ Config.getLibraries().forEach((entry) -> {
+ Label name = new Label(entry.getKey());
+ name.getStyleClass().add("sub-menu-item");
+ name.setOnMouseClicked((event) -> {
+ selectLibrary(entry.getKey());
+ });
+ libraryMenu.getChildren().add(0, name);
+ });
+ }
+
+ private void interact(Folder folder) {
+ if (folder.hasSubfolders()) {
+ searchBox.signal(folder);
+ scrollPane.setVvalue(0.0);
+ Thumbnails.requestExpress(folder);
+ } else {
+ Stopwatch.click("Starting new JVM");
+
+ String file = folder.getVolumes().get(0).getFilename();
+
+ KiwiApplication.startReader(file);
+ }
+ }
+
+ private void addPanels(Folder folder) {
+ folderQueue.clear();
+ queueFolder(folder);
+
+ Platform.runLater(() -> {
+ new ThreadPanelGen(folderQueue, flowPane, this).start();
+ });
+ }
+
+ private void queueFolder(Folder folder) {
+ folderQueue.add(folder);
+ folder.getSubfolders().forEach((child) -> {
+ queueFolder(child);
+ });
+ }
+
+ private void checkInBounds() {
+
+ Bounds viewport = scrollPane.getViewportBounds();
+ double contentHeight = scrollPane.getContent().getBoundsInLocal().getHeight();
+ double nodeMinY = selected.getBoundsInParent().getMinY();
+ double nodeMaxY = selected.getBoundsInParent().getMaxY();
+ double viewportMinY = (contentHeight - viewport.getHeight()) * scrollPane.getVvalue();
+ double viewportMaxY = viewportMinY + viewport.getHeight();
+ if (nodeMinY < viewportMinY) {
+ scrollPane.setVvalue(nodeMinY / (contentHeight - viewport.getHeight()));
+ } else if (nodeMaxY > viewportMaxY) {
+ scrollPane.setVvalue((nodeMaxY - viewport.getHeight()) / (contentHeight - viewport.getHeight()));
+ }
+ }
+
+ private void setMenuParent(Label parent, Pane child) {
+ parent.setOnMouseClicked((event) -> {
+ toggleSubmenu(child);
+ });
+ toggleSubmenu(child);
+
+ }
+
+ private FolderPanel getSingleVisible() {
+ int found = 0;
+ FolderPanel panel = null;
+ for (Node node : flowPane.getChildren()) {
+ FolderPanel p = (FolderPanel) (node);
+ if (!p.hidden) {
+ panel = p;
+ found++;
+ }
+ }
+
+ if (found > 1) {
+ return null;
+ }
+
+ return panel;
+ }
+
+ private void loadLayout() {
+ FXMLLoader loader = new FXMLLoader(Resources.get(FXML_FILE));
+ loader.setRoot(this);
+ loader.setController(this);
+
+ try {
+ loader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/com/proxy/kiwi/explorer/KiwiLoader.java b/src/com/proxy/kiwi/explorer/KiwiLoader.java
new file mode 100644
index 0000000..2ee0f4b
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/KiwiLoader.java
@@ -0,0 +1,35 @@
+package com.proxy.kiwi.explorer;
+
+import com.proxy.kiwi.core.utils.Resources;
+import javafx.application.Platform;
+import javafx.application.Preloader;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+
+public class KiwiLoader extends Preloader {
+
+ public static Stage stage = null;
+
+ @Override
+ public void start(Stage primaryStage) throws Exception {
+ BorderPane pane = (BorderPane) FXMLLoader.load(Resources.get("loading.fxml"));
+ primaryStage.setScene(new Scene(pane, 600, 400));
+ primaryStage.setResizable(false);
+
+ primaryStage.initStyle(StageStyle.UNDECORATED);
+
+ primaryStage.show();
+ stage = primaryStage;
+ }
+
+ public static void hide() {
+ Platform.runLater(() -> {
+ if (stage != null) {
+ stage.hide();
+ }
+ });
+ }
+}
diff --git a/src/com/proxy/kiwi/explorer/SearchBox.java b/src/com/proxy/kiwi/explorer/SearchBox.java
new file mode 100644
index 0000000..805e183
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/SearchBox.java
@@ -0,0 +1,281 @@
+package com.proxy.kiwi.explorer;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.Folders;
+import com.proxy.kiwi.core.utils.Command;
+import com.proxy.kiwi.core.utils.Resources;
+
+import javafx.application.Platform;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.StringProperty;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.HBox;
+
+public class SearchBox extends HBox{
+
+ static final String TAG_PREFIX = ":";
+ static final String FXML_FILE = "search_box.fxml";
+ static final int MIN_TAG_LENGTH = 3;
+
+
+ LinkedList parents;
+ LinkedList tags;
+
+ LinkedList labels;
+
+ Set allTags;
+
+ IntegerProperty terms;
+
+ private Runnable onSingleHandler;
+ private Runnable onChange;
+ Node onEmptyFocus;
+
+ @FXML private TextField searchField;
+ @FXML private HBox searchTags;
+
+ public SearchBox() {
+ loadLayout();
+ Platform.runLater(this::init);
+ init();
+ }
+
+ public void init() {
+ Platform.runLater( ()-> {
+ this.prefWidthProperty().bind(this.getScene().getWindow().widthProperty().subtract(300));
+ });
+
+ parents = new LinkedList<>();
+
+ parents.add(Folders.getRoot());
+ tags = new LinkedList<>();
+
+ terms = new SimpleIntegerProperty(1);
+
+ labels = new LinkedList<>();
+
+ allTags = Config.getTags();
+
+ searchField.setOnKeyPressed((event) -> {
+ if (event.getCode().equals(KeyCode.TAB)) {
+ //TODO autocomplete
+ } else if (Config.getCommandFor(event.getCode()).equals(Command.BACK)
+ && searchField.textProperty().getValue().length() == 0) {
+ signalBack();
+ } else if (Config.getCommandFor(event.getCode()).equals(Command.ENTER)) {
+ String search = searchField.textProperty().getValueSafe().toLowerCase().trim();
+ if (search.startsWith(TAG_PREFIX) && search.length() > 1) {
+ String tag = search.substring(TAG_PREFIX.length()).trim().toLowerCase();
+
+ System.out.println(tag);
+ long possibleTags = allTags.stream().filter(s -> !tags.contains(s)).filter(s -> s.toLowerCase().contains(tag)).count();
+ System.out.println(possibleTags);
+ boolean canAddTag = (possibleTags == 1);
+
+ if (canAddTag) {
+ String singleTag = allTags.stream().filter(s -> !tags.contains(s)).filter(s -> s.toLowerCase().contains(tag)).findFirst().get();
+ tags.add(singleTag);
+ addTagFolder(singleTag);
+ tryChange();
+ searchField.setText("");
+ }
+
+ }
+ if (onSingleHandler != null) {
+ onSingleHandler.run();
+ }
+
+ }
+ });
+
+ }
+
+ @FXML
+ public void handleInputKeyEvent(KeyEvent event) {
+ if (searchField.getText().trim().length() == 0) {
+ switch (Config.getCommandFor(event.getCode())) {
+ case LEFT:
+ case RIGHT:
+ case DOWN:
+ case UP:
+ if (onEmptyFocus != null) {
+ onEmptyFocus.requestFocus();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ event.consume();
+ }
+
+ public void onEmptyFocus(Node node) {
+ this.onEmptyFocus = node;
+ }
+
+ public void onSingleInteract(Runnable r) {
+ onSingleHandler = r;
+ }
+
+ public boolean accept(FolderPanel panel, boolean collapse) {
+ allTags = Config.getTags();
+
+
+ Folder folder = panel.folder;
+ Set folderTags = Config.getTags(folder);
+
+ if (folder.getParent() == null) {
+ return false;
+ }
+
+ String search = searchField.textProperty().getValueSafe().toLowerCase().trim();
+
+ collapse = collapse || ( search.startsWith(TAG_PREFIX) && search.length() > MIN_TAG_LENGTH) || !tags.isEmpty();
+
+ boolean parentValid = (parents.isEmpty() || folder.getParent().equals(parents.getLast()));
+ boolean anscestorValid = (parents.isEmpty() || folder.hasAncestor(parents.getLast()));
+
+ boolean folderValid = (collapse && anscestorValid) || (!collapse && parentValid);
+
+ boolean tagsValid = (tags.isEmpty() || folderTags.containsAll(tags));
+
+ Set remainingTags = folderTags.stream()
+ .filter( (s) -> (!tags.contains(s)))
+ .collect(Collectors.toSet());
+
+ boolean searchValid = false;
+
+ if (search.startsWith(TAG_PREFIX)) {
+ String tSearch = search.substring(TAG_PREFIX.length());
+ searchValid = remainingTags.stream().anyMatch((s) -> s.toLowerCase().contains(tSearch));
+ } else {
+ if (collapse) {
+ searchValid = search.length() == 0 || folder.getName().toLowerCase().contains(search) || folder.hasAncestorWith(search);
+ } else {
+ searchValid = search.length() == 0 || folder.getName().toLowerCase().contains(search);
+ }
+
+ }
+
+ boolean isValid = folderValid && tagsValid && searchValid;
+
+ return isValid;
+ }
+
+ public StringProperty getQuery() {
+ return searchField.textProperty();
+ }
+
+ public IntegerProperty getTerms() {
+ return terms;
+ }
+
+ private void addFolder(String name) {
+ Label l = new Label(name);
+ labels.add(new LabelTuple(LabelType.FOLDER, l));
+ searchTags.getChildren().add(searchTags.getChildren().size() - 1, l);
+ tryChange();
+ }
+
+ private void removeLastFolder() {
+ if (parents.size() > 1) {
+ parents.removeLast();
+ for (int i = labels.size() -1 ; i >= 0; i--) {
+ if (labels.get(i).type == LabelType.FOLDER) {
+ searchTags.getChildren().remove(labels.get(i).label);
+ labels.remove(i);
+ tryChange();
+ break;
+ }
+ }
+ }
+ }
+
+ private void addTagFolder(String name) {
+ Label l = new Label(name);
+ labels.add(new LabelTuple(LabelType.TAG, l));
+ searchTags.getChildren().add(searchTags.getChildren().size() - 1, l);
+ tryChange();
+ }
+
+ private void removeLastTag() {
+ if (!tags.isEmpty()) {
+ tags.removeLast();
+ for (int i = labels.size() -1 ; i >= 0; i--) {
+ if (labels.get(i).type == LabelType.TAG) {
+ searchTags.getChildren().remove(labels.get(i).label);
+ labels.remove(i);
+ tryChange();
+ break;
+ }
+ }
+ }
+ }
+
+ public synchronized void signal(Folder folder) {
+ addFolder(folder.getName());
+ parents.add(folder);
+ searchField.setText("");
+ }
+
+ public synchronized void signalBack() {
+ if (parents.size() > 1) {
+ removeLastFolder();
+ } else if (tags.size() > 0) {
+ removeLastTag();
+ }
+ tryChange();
+ }
+
+ public void tryChange() {
+ Platform.runLater(() -> {
+ if (onChange != null) {
+ onChange.run();
+ }
+ });
+ }
+
+ public void onChange(Runnable onChange) {
+ this.onChange = onChange;
+ }
+
+ private void loadLayout() {
+ FXMLLoader loader = new FXMLLoader(Resources.get(FXML_FILE));
+ loader.setRoot(this);
+ loader.setController(this);
+
+ try {
+ loader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
+enum LabelType {
+ TAG,FOLDER;
+}
+
+class LabelTuple {
+ public LabelType type;
+ public Label label;
+
+ public LabelTuple(LabelType type, Label label) {
+ super();
+ this.type = type;
+ this.label = label;
+ }
+}
diff --git a/src/com/proxy/kiwi/explorer/ThreadPanelGen.java b/src/com/proxy/kiwi/explorer/ThreadPanelGen.java
new file mode 100644
index 0000000..d345991
--- /dev/null
+++ b/src/com/proxy/kiwi/explorer/ThreadPanelGen.java
@@ -0,0 +1,52 @@
+package com.proxy.kiwi.explorer;
+
+import com.proxy.kiwi.core.folder.Folder;
+
+import javafx.application.Platform;
+import javafx.scene.layout.FlowPane;
+
+import java.util.Queue;
+
+public class ThreadPanelGen extends Thread {
+ Queue folderQueue;
+ FlowPane pane;
+ KiwiExplorerPane controller;
+
+ public ThreadPanelGen(Queue folderQueue, FlowPane flowPane, KiwiExplorerPane controller) {
+ this.folderQueue = folderQueue;
+ this.pane = flowPane;
+ this.controller = controller;
+ }
+
+ public void run() {
+ int size = folderQueue.size();
+ int processed = 0;
+
+ while (!folderQueue.isEmpty()) {
+ Folder f = folderQueue.remove();
+
+ processed++;
+ double percent = (1.0 * processed) / (1.0 * size);
+
+ Platform.runLater(() -> {
+ FolderPanel p = new FolderPanel(f, controller);
+
+ pane.getChildren().add(p);
+ controller.loadingBar.setProgress(percent);
+ controller.loadingLabel.setText(f.getFilenameProperty().get());
+ controller.updateLast();
+
+ if (percent == 1) {
+ controller.loadingBox.setVisible(false);
+ }
+ });
+
+ try {
+ sleep(2);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/reader/ChapterLabel.java b/src/com/proxy/kiwi/reader/ChapterLabel.java
new file mode 100644
index 0000000..4627294
--- /dev/null
+++ b/src/com/proxy/kiwi/reader/ChapterLabel.java
@@ -0,0 +1,42 @@
+package com.proxy.kiwi.reader;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.scene.control.Label;
+import javafx.scene.paint.Color;
+
+public class ChapterLabel extends Label {
+
+ int page, nextPage;
+
+ public ChapterLabel(String pageStr, String nextPageStr, IntegerProperty currentPage) {
+ super(pageStr);
+
+ getStyleClass().add("text");
+
+ try {
+ this.page = Integer.parseInt(pageStr.trim());
+ } catch (NumberFormatException e) {
+ this.page = 0;
+ }
+ try {
+ this.nextPage = Integer.parseInt(nextPageStr.trim());
+ } catch (NumberFormatException e) {
+ this.nextPage = Integer.MAX_VALUE;
+ }
+
+ refresh(currentPage.getValue());
+
+ currentPage.addListener((obs, old, val) -> {
+ refresh(val);
+ });
+
+ }
+
+ public void refresh(Number pagenum) {
+ if (pagenum.doubleValue() >= page && (pagenum.doubleValue() < nextPage)) {
+ this.setTextFill(Color.CRIMSON);
+ } else {
+ this.setTextFill(Color.BLACK);
+ }
+ }
+}
diff --git a/src/com/proxy/kiwi/reader/KiwiReadingPane.java b/src/com/proxy/kiwi/reader/KiwiReadingPane.java
new file mode 100644
index 0000000..ac6573d
--- /dev/null
+++ b/src/com/proxy/kiwi/reader/KiwiReadingPane.java
@@ -0,0 +1,404 @@
+package com.proxy.kiwi.reader;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+import javax.imageio.ImageIO;
+
+import org.imgscalr.Scalr;
+import org.imgscalr.Scalr.Method;
+import org.imgscalr.Scalr.Mode;
+
+import com.proxy.kiwi.app.KiwiApplication;
+import com.proxy.kiwi.core.folder.Folder;
+import com.proxy.kiwi.core.image.KiwiImage;
+import com.proxy.kiwi.core.image.Orientation;
+import com.proxy.kiwi.core.services.Config;
+import com.proxy.kiwi.core.services.Folders;
+import com.proxy.kiwi.core.utils.FXTools;
+import com.proxy.kiwi.core.utils.Log;
+import com.proxy.kiwi.core.utils.Resources;
+import com.proxy.kiwi.core.utils.Stopwatch;
+
+import javafx.application.Platform;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Group;
+import javafx.scene.control.Label;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.WritableImage;
+import javafx.scene.input.Dragboard;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.TransferMode;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Translate;
+import javafx.stage.Stage;
+
+public class KiwiReadingPane extends StackPane{
+
+ static final String FXML_FILE = "reading_pane.fxml";
+
+ @FXML private ImageView view;
+ @FXML private Label pagenum;
+ @FXML private VBox chapters;
+ @FXML private Group group;
+
+ private Rotate rotate;
+ private Translate translate;
+
+ private volatile Folder folder;
+ private KiwiImage image;
+ private WritableImage fullImage;
+
+ private Stage stage;
+
+ private volatile SimpleIntegerProperty pageProperty = new SimpleIntegerProperty(1);
+ private SimpleDoubleProperty zoomHeightRatio = new SimpleDoubleProperty(1.0);
+ private SimpleDoubleProperty zoomWidthRatio = new SimpleDoubleProperty(1.0);
+ private SimpleDoubleProperty heightRatio = new SimpleDoubleProperty(1.0);
+ private SimpleDoubleProperty widthRatio = new SimpleDoubleProperty(1.0);
+
+ private boolean isChangingPage;
+
+ private int pageNumber;
+
+ private Method resamplingMethod;
+
+ private SimpleStringProperty titleProperty = new SimpleStringProperty();
+ private SimpleStringProperty folderNameProperty = new SimpleStringProperty();
+
+ // TODO move constants to separate place?
+ private static final double ZOOM_RATIO_INCREMENT = 0.2, TRANSLATE_Y_RATE = 400;
+
+ public KiwiReadingPane(Stage stage, String path) {
+
+ this.stage = stage;
+
+ resamplingMethod = Method.QUALITY;
+
+ CompletableFuture folderInit = CompletableFuture.runAsync(() -> {
+
+ folder = Folder.fromFile(path);
+
+ folder.load();
+
+ titleProperty.bind(new SimpleStringProperty("Kiwi - ").concat(folderNameProperty)
+ .concat(" - ").concat(pageProperty.asString()));
+
+ stage.titleProperty().bind(titleProperty);
+
+ pageNumber = Folders.find(folder, path);
+ });
+
+ loadLayout();
+
+ rotate = new Rotate(0);
+ translate = new Translate(0, 0);
+
+ group.getTransforms().addAll(rotate, translate);
+
+ view.fitWidthProperty().bind(this.widthProperty().multiply(zoomWidthRatio).multiply(widthRatio));
+ view.fitHeightProperty().bind(this.heightProperty().multiply(zoomHeightRatio).multiply(heightRatio));
+ view.preserveRatioProperty().setValue(true);
+
+ view.setSmooth(false);
+
+
+ CompletableFuture.allOf(folderInit).thenRun(() -> {
+ Platform.runLater(() -> {
+ pageProperty.set(pageNumber);
+
+ pagenum.textProperty().bind(pageProperty.asString()
+ .concat(new SimpleStringProperty("/").concat(folder.getSizeProperty().asString())));
+
+ folderNameProperty.set(folder.getName());
+ changePage(pageNumber);
+ setChapters();
+
+ ResizeListener listener = new ResizeListener(100) {
+
+ @Override
+ public void onResizeStart() {
+ view.setImage(image);
+ }
+
+ @Override
+ public void onResizeEnd() {
+ loadImage();
+ }
+
+ };
+
+ this.widthProperty().addListener(listener);
+ this.heightProperty().addListener(listener);
+
+ stage.getScene().setOnDragOver(event -> {
+ Dragboard db = event.getDragboard();
+ if (db.hasFiles()) {
+ event.acceptTransferModes(TransferMode.MOVE);
+ } else {
+ event.consume();
+ }
+ });
+
+ stage.getScene().setOnDragDropped(event -> {
+ Dragboard db = event.getDragboard();
+ if (db.hasFiles()) {
+ File file = db.getFiles().get(0);
+ Folder folder = Folder.fromFile(file);
+ if (folder.equals(this.folder)) {
+ changePage(folder.find(file.getAbsolutePath()));
+ } else {
+ setFolder(folder);
+ changePage(folder.find(file.getAbsolutePath()));
+ }
+ }
+ });
+ });
+ });
+ }
+
+ @FXML
+ public void handleKeyPress(KeyEvent event) {
+ switch (Config.getCommandFor(event.getCode())) {
+ case LEFT:
+ changePage(pageProperty.get() - 1);
+ break;
+ case RIGHT:
+ changePage(pageProperty.get() + 1);
+ break;
+ case NEXT_FOLDER:
+ Folder next = folder.next();
+ Log.print(Log.EVENT, "Switching folder to " + next.getName());
+
+ setFolder(next);
+ changePage(1);
+ break;
+ case PREVIOUS_FOLDER:
+ Folder previous = folder.previous();
+ Log.print(Log.EVENT, "Switching folder to " + previous.getName());
+ setFolder(previous);
+ changePage(1);
+ break;
+ case CHAPTER_NEXT:
+ if (Config.getChapters(folder.getName()) != null) {
+ changePage(Config.getNextChapter(folder.getName(), pageProperty.get()));
+ }
+ break;
+ case CHAPTER_PREVIOUS:
+ if (Config.getChapters(folder.getName()) != null) {
+ changePage(Config.getPreviousChapter(folder.getName(), pageProperty.get()));
+ }
+ break;
+ case UP:
+ addTranslateY(TRANSLATE_Y_RATE);
+ break;
+ case DOWN:
+ addTranslateY(-TRANSLATE_Y_RATE);
+ break;
+ case ZOOM_IN:
+ addZoom(ZOOM_RATIO_INCREMENT);
+ break;
+ case ZOOM_OUT:
+ addZoom(-ZOOM_RATIO_INCREMENT);
+ break;
+ case CHAPTER_ADD:
+ Config.addFolderChapter(folder.getName(), pageProperty.get());
+ setChapters();
+ break;
+ case CHAPTER_REMOVE:
+ Config.removeFolderChapter(folder.getName(), pageProperty.get());
+ setChapters();
+ break;
+ case MINIMIZE:
+ stage.setIconified(true);
+ break;
+ case QUALITY:
+ resamplingMethod = (resamplingMethod == Method.QUALITY ? Method.SPEED : Method.QUALITY);
+ Log.print(Log.EVENT, "Quality set to " + resamplingMethod.toString());
+ break;
+ case FULL_SCREEN:
+ event.consume();
+ stage.setFullScreen(!stage.isFullScreen());
+ break;
+ case EXIT:
+ event.consume();
+ stage.hide();
+ KiwiApplication.exit();
+ break;
+ default:
+ break;
+ }
+ }
+
+ public IntegerProperty getPage() {
+ return pageProperty;
+ }
+
+ public Folder getFolder() {
+ return folder;
+ }
+
+ public void setFolder(Folder folder) {
+ this.folder = folder;
+
+ folderNameProperty.set(folder.getName());
+
+ pagenum.textProperty().bind(pageProperty.asString()
+ .concat(new SimpleStringProperty("/").concat(folder.getSizeProperty().asString())));
+
+
+ setChapters();
+ }
+
+ public void loadImage() {
+
+ double w = view.getFitWidth();
+ double h = view.getFitHeight();
+
+ int page = pageProperty.get();
+
+ File file = folder.getVolumes().get(page - 1).getFile();
+
+ try {
+ // FIXME add .jpg compatibility
+ BufferedImage image = ImageIO.read(file);
+
+ BufferedImage result = Scalr.resize(image, resamplingMethod, Mode.AUTOMATIC, (int) w, (int) h,
+ (BufferedImageOp[]) null);
+
+ if (fullImage != null) {
+ fullImage = null;
+ System.gc();
+ }
+ fullImage = SwingFXUtils.toFXImage(result, null);
+
+ view.setImage(fullImage);
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+
+ public void changePage(int page) {
+ if (page <= 0 || page > folder.getVolumes().size() || isChangingPage) {
+ return;
+ }
+ Stopwatch.click("Changing page");
+ isChangingPage = true;
+ Platform.runLater(() -> {
+
+ this.pageProperty.set(page);
+
+ File file = folder.getVolumes().get(page - 1).getFile();
+
+ image = null;
+ System.gc();
+
+ Stopwatch.click("Loading Image");
+
+
+
+
+ image = new KiwiImage(file);
+
+ Orientation orientation = image.getOrientation();
+
+ double width = image.getWidth();
+ double height = image.getHeight();
+
+ heightRatio.set(orientation.getHeightRatio(width, height));
+ widthRatio.set(orientation.getWidthRatio(width, height));
+ view.setRotate(orientation.getRotation());
+
+ if (resamplingMethod != Method.SPEED) {
+ loadImage();
+ } else {
+ view.setImage(image);
+ }
+
+ Stopwatch.click("Loading Image");
+
+ resetYOffset();
+
+ isChangingPage = false;
+ Stopwatch.click("Changing page");
+ });
+ }
+
+ private void setChapters() {
+ chapters.getChildren().clear();
+
+ String[] chapternums = Config.getChapters(folder.getName());
+ if (chapternums != null) {
+
+ for (int i = 0; i < chapternums.length; i++) {
+ ChapterLabel label = new ChapterLabel(chapternums[i],
+ (i < chapternums.length - 1 ? chapternums[i + 1] : ""), pageProperty);
+
+ chapters.getChildren().add(label);
+ }
+ }
+ }
+
+ private void resetYOffset() {
+ translate.setY(getYLimit());
+ }
+
+ private void addZoom(double zoom) {
+ zoomHeightRatio.set(zoomHeightRatio.get() + zoom);
+ checkTranslateY();
+ }
+
+ private int getYLimit() {
+ return (int) ((zoomHeightRatio.get() - 1) * this.getScene().getHeight() / 2);
+ }
+
+ private void checkTranslateY() {
+ int lim = getYLimit();
+ if (translate.getY() < -lim) {
+ translate.setY(-lim);
+ }
+ if (translate.getY() > lim) {
+ translate.setY(lim);
+ }
+ }
+
+ private void addTranslateY(double dy) {
+ if (zoomHeightRatio.get() >= 1) {
+
+ translate.setY(translate.getY() + dy);
+ checkTranslateY();
+ }
+ }
+
+ private void loadLayout() {
+
+ Object self = this;
+
+ FXTools.runAndWait( () -> {
+ Stopwatch.click("Loading Layout");
+ FXMLLoader loader = new FXMLLoader(Resources.get(FXML_FILE));
+ loader.setRoot(self);
+ loader.setController(self);
+
+ try {
+ loader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Stopwatch.click("Loading Layout");
+ });
+ }
+}
diff --git a/src/com/proxy/kiwi/reader/ResizeListener.java b/src/com/proxy/kiwi/reader/ResizeListener.java
new file mode 100644
index 0000000..19cc11b
--- /dev/null
+++ b/src/com/proxy/kiwi/reader/ResizeListener.java
@@ -0,0 +1,49 @@
+package com.proxy.kiwi.reader;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+
+public abstract class ResizeListener implements ChangeListener {
+
+ long delay;
+ final Timer timer;
+ TimerTask task;
+ boolean started;
+
+ public ResizeListener(long delay) {
+ this.delay = delay;
+ timer = new Timer();
+ task = null;
+ started = false;
+ }
+
+ public void changed(ObservableValue extends Number> observable, Number oldNum, Number newNum) {
+ if (task != null) {
+ task.cancel();
+ }
+
+ if (!started) {
+ started = true;
+ onResizeStart();
+ }
+
+ task = new TimerTask() {
+
+ @Override
+ public void run() {
+ onResizeEnd();
+ started = false;
+ }
+
+ };
+
+ timer.schedule(task, delay);
+ }
+
+ public abstract void onResizeStart();
+
+ public abstract void onResizeEnd();
+}
diff --git a/src/com/proxy/kiwi/res/application.css b/src/com/proxy/kiwi/res/application.css
new file mode 100644
index 0000000..cd1ad40
--- /dev/null
+++ b/src/com/proxy/kiwi/res/application.css
@@ -0,0 +1,96 @@
+/* JavaFX CSS - Leave this comment until you have at least create one rule which uses -fx-Property */
+
+.text {
+ -fx-font-family: "Ubuntu Bold";
+}
+
+.large {
+ -fx-font-size: 18;
+}
+
+.fa {
+ -fx-font-family: "FontAwesome";
+}
+
+.menu {
+ -fx-background-color: rgba(200,200,200,1);
+}
+
+.scroll-menu > .viewport {
+ -fx-background-color: rgba(200,200,200,1);
+}
+
+.menu-item{
+ -fx-pref-width: 175;
+}
+
+.right-menu-item{
+ -fx-pref-width: 10000;
+ -fx-alignment: center;
+ -fx-font-family: "Ubuntu Bold";
+ -fx-font-size: 20;
+ -fx-padding: 15;
+}
+
+
+
+.sub-menu-item {
+ -fx-pref-width: 10000;
+ -fx-alignment: center;
+ -fx-font-family: "Ubuntu Bold";
+ -fx-font-size: 16;
+ -fx-padding: 15;
+}
+
+.right-menu-item:hover, .sub-menu-item:hover, .right-menu-item:selected, .right-menu-item:focused {
+ -fx-background-color: rgba(0,255,0,0.5);
+ -fx-cursor: hand;
+}
+
+.folder-title {
+ -fx-alignment: center;
+ -fx-text-alignment: center;
+ -fx-font-size: 18;
+ -fx-text-fill: white;
+ -fx-wrap-text: true;
+ -fx-content-display: bottom;
+ -fx-padding: 0 10 0 10;
+}
+
+.tags Label{
+ -fx-background-color: rgb(187,187,187);
+ -fx-background-radius: 5px;
+ -fx-padding: 5px;
+}
+
+.scroll-pane .scroll-bar:vertical {
+ -fx-unit-increment: 160 ;
+ -fx-block-increment: 300;
+}
+
+
+.clickable {
+ -fx-opacity: 0.5;
+}
+
+.clickable:hover {
+ -fx-opacity: 1.0;
+}
+
+.settings-btn{
+ -fx-background-image: url("options.png");
+ -fx-background-size: 50 50;
+ -fx-background-repeat: no-repeat;
+ -fx-background-position: center;
+ -fx-background-color: rgba(1.0,1.0,1.0,0.0);
+ -fx-opacity: 0.2;
+}
+
+.settings-btn:hover{
+ -fx-opacity: 1.0;
+}
+
+.chapters ChapterLabel {
+ -fx-font-size: 20;
+ -fx-pref-height: 10000;
+}
\ No newline at end of file
diff --git a/src/com/proxy/kiwi/res/default_config.txt b/src/com/proxy/kiwi/res/default_config.txt
new file mode 100644
index 0000000..959d98f
--- /dev/null
+++ b/src/com/proxy/kiwi/res/default_config.txt
@@ -0,0 +1,11 @@
+{
+ 'path': '',
+ 'libraries': { },
+ 'item_width': '250',
+ 'item_height': '362',
+ 'width': '1600',
+ 'height': '900',
+ 'columns': '6',
+ 'hotkeys': { },
+ 'folders' : { }
+}
diff --git a/src/com/proxy/kiwi/res/editmenu.fxml b/src/com/proxy/kiwi/res/editmenu.fxml
new file mode 100644
index 0000000..93d2439
--- /dev/null
+++ b/src/com/proxy/kiwi/res/editmenu.fxml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/proxy/kiwi/res/explorer_pane.fxml b/src/com/proxy/kiwi/res/explorer_pane.fxml
new file mode 100644
index 0000000..13e77dd
--- /dev/null
+++ b/src/com/proxy/kiwi/res/explorer_pane.fxml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/proxy/kiwi/res/fonts/CONTRIBUTING.txt b/src/com/proxy/kiwi/res/fonts/CONTRIBUTING.txt
new file mode 100644
index 0000000..15bdc0c
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/CONTRIBUTING.txt
@@ -0,0 +1,21 @@
+The Ubuntu Font Family is very long-term endeavour, and the first time
+that a professionally-designed font has been funded specifically with
+the intent of being an on-going community expanded project:
+
+ http://font.ubuntu.com/
+
+Development of the Ubuntu Font Family is undertaken on Launchpad:
+
+ http://launchpad.net/ubuntu-font-family/
+
+and this is where milestones, bug management and releases are handled.
+
+Contributions are welcomed. Your work will be used on millions of
+computers every single day! Following the initial bootstrapping of
+Latin, Cyrillic, Greek, Arabic and Hebrew expansion will be undertaken
+by font designers from the font design and Ubuntu communities.
+
+To ensure that the Ubuntu Font Family can be re-licensed to future
+widely-used libre font licences, copyright assignment is being required:
+
+ https://launchpad.net/~uff-contributors
diff --git a/src/com/proxy/kiwi/res/fonts/FONTLOG.txt b/src/com/proxy/kiwi/res/fonts/FONTLOG.txt
new file mode 100644
index 0000000..83022be
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/FONTLOG.txt
@@ -0,0 +1,292 @@
+This is the FONTLOG file for the Ubuntu Font Family and attempts to follow
+the recommendations at: http://scripts.sil.org/OFL-FAQ_web#43cecb44
+
+
+Overview
+
+The new Ubuntu Font Family was started to enable the personality of
+Ubuntu to be seen and felt in every menu, button and dialog.
+The typeface is sans-serif, uses OpenType features and is manually
+hinted for clarity on desktop and mobile computing screens.
+
+The scope of the Ubuntu Font Family includes all the languages used by
+the various Ubuntu users around the world in tune with Ubuntu's
+philosophy which states that every user should be able to use their
+software in the language of their choice. So the Ubuntu Font Family
+project will be extended to cover many more written languages.
+
+
+History
+
+The Ubuntu Font Family has been creating during 2010 and 2011. As of
+September 2011 coverage is provided for Latin, Cyrillic and Greek across
+Regular, Italic, Bold and Bold-Italic. Further work was uptaken during
+2015.
+
+
+ChangeLog
+
+2015-08-21 (Paul Sladen) Ubuntu Font Family version 0.83
+
+ Note: This release was created by binary patching from the v0.80
+ release using the scripts in 'sources/patch-0.80-0.83/' to rebuild
+ the necessary tables. The release selectively updates only those
+ proportional .ttf font files exhibiting the bug below bug number;
+ the Ubuntu Mono monospace font files remain unchanged, being the
+ original version 0.80 ones.
+
+ [Marc Foley]
+ * [Engineering] Fixed wrong characters appear in some mac apps. (LP: #1334363)
+
+
+2011-09-22 (Paul Sladen) Ubuntu Font Family version 0.80
+
+ [Vincent Connare/Dalton Maag]
+ * Wish for addition of a monospaced member to the family (LP: #640382)
+ * Mono: No hinting yet - Ubuntu Beta Mono font looks jagged in
+ Netbeans and terrible with ClearType (LP: #820493)
+ * Emacs: choosing normal monospace font in Emacs but gives bold-italic
+ (LP: #791076)
+ * PUA: ensure that Ubuntu Circle of Friends logo is full size: (LP: #853855)
+ + U+E0FF becomes large size in proportionals, remains small width in
+ monospaces
+ + U+F0FF becomes small size (proportionals only)
+ + U+F200 is full ubuntu logomark (proportionals only)
+
+ [Paul Sladen]
+ * Monospace: Patch Family Name to be "Ubuntu Mono"
+ * Monospace: Patch U+EFFD version debugging glyph to be '0.8'
+
+ [Cody Boisclair]
+ * Monospace: Force .null HDMX advance to 500
+ * Monospace: Remap ASCII box-drawing characters (LP: #788757)
+
+ [Júlio Reis]
+ * Date corrections to 'FONTLOG' (LP: #836595)
+
+2011-03-08 (Paul Sladen) Ubuntu Font Family version 0.71.2
+
+ * (Production) Adjust Medium WeightClass to 500 (Md, MdIt) (LP: #730912)
+
+2011-03-07 (Paul Sladen) Ubuntu Font Family version 0.71.1
+
+ * (Design) Add Capitalised version of glyphs and kern. (Lt, LtIt,
+ Md, MdIt) DM (LP: #677446)
+ * (Design) Re-space and tighen Regular and Italic by amount specified
+ by Mark Shuttleworth (minus 4 FUnits). (Rg, It) (LP: #677149)
+ * (Design) Design: Latin (U+0192) made straight more like l/c f with
+ tail (LP: #670768)
+ * (Design) (U+01B3) should have hook on right, as the lowercase
+ (U+01B4) (LP: #681026)
+ * (Design) Tail of Light Italic germandbls, longs and lowercase 'f'
+ to match Italic/BoldItalic (LP: #623925)
+ * (Production) Update feature (Lt, LtIt, Md, MdIt). DM
+ (LP: #676538, #676539)
+ * (Production) Remove Bulgarian locl feature for Italics. (LP: #708578)
+ * (Production) Update Description information with new string:
+ "The Ubuntu Font Family are libre fonts funded by Canonical Ltd
+ on behalf of the Ubuntu project. The font design work and
+ technical implementation is being undertaken by Dalton Maag. The
+ typeface is sans-serif, uses OpenType features and is manually
+ hinted for clarity on desktop and mobile computing screens. The
+ scope of the Ubuntu Font Family includes all the languages used
+ by the various Ubuntu users around the world in tune with
+ Ubuntu's philosophy which states that every user should be able
+ to use their software in the language of their choice. The
+ project is ongoing, and we expect the family will be extended to
+ cover many written languages in the coming years."
+ (Rg, It, Bd, BdIt, Lt, LtIt, Md, MdIt) (LP: #690590)
+ * (Production) Pixel per em indicator added at U+F000 (Lt, LtIt, Md,
+ MdIt) (LP: #615787)
+ * (Production) Version number indicator added at U+EFFD (Lt, LtIt, Md,
+ MdIt) (LP: #640623)
+ * (Production) fstype bit set to 0 - Editable (Lt, LtIt, Md, MdIt)
+ (LP: #648406)
+ * (Production) Localisation of name table has been removed because
+ of problems with Mac OS/X interpretation of localisation. DM
+ (LP: #730785)
+ * (Hinting) Regular '?' dot non-circular (has incorrect control
+ value). (LP: #654336)
+ * (Hinting) Too much space after latin capital 'G' in 13pt
+ regular. Now reduced. (LP: #683437)
+ * (Hinting) Balance Indian Rupee at 18,19pt (LP: #662177)
+ * (Hinting) Make Regular '£' less ambiguous at 13-15 ppm (LP: #685562)
+ * (Hinting) Regular capital 'W' made symmetrical at 31 ppem (LP: #686168)
+
+2010-12-14 (Paul Sladen) Ubuntu Font Family version 0.70.1
+
+ Packaging, rebuilt from '2010-12-08 UbuntuFontsSourceFiles_070.zip':
+ * (Midstream) Fstype bit != 0 (LP: #648406)
+ * (Midstream) Add unit test to validate fstype bits (LP: #648406)
+ * (Midstream) Add unit test to validate licence
+
+2010-12-14 (Paul Sladen) Ubuntu Font Family version 0.70
+
+ Release notes 0.70:
+ * (Design) Add Capitalised version of glyphs and kern. (Rg, It, Bd,
+ BdIt) DM (LP: #676538, #677446)
+ * (Design) Give acute and grave a slight upright move to more match
+ the Hungarian double acute angle. (Rg, It, Bd, BdIt) (LP: #656647)
+ * (Design) Shift Bold Italic accent glyphs to be consistent with the
+ Italic. (BdIt only) DM (LP: #677449)
+ * (Design) Check spacing and kerning of dcaron, lcaron and
+ tcaron. (Rg, It, Bd, BdIt) (LP: #664722)
+ * (Design) Add positive kerning to () {} [] to open out the
+ combinations so they are less like a closed box. (Rg, It, Bd,
+ BdIt) (LP: #671228)
+ * (Design) Change design of acute.asc and check highest points (Bd
+ and BdIt only) DM
+ * (Production) Update feature. DM (LP: #676538, #676539)
+ * (Production) Remove Romanian locl feature. (Rg, It, Bd, BdIt)
+ (LP: #635615)
+ * (Production) Update Copyright information with new
+ strings. "Copyright 2010 Canonical Ltd. Licensed under the Ubuntu
+ Font Licence 1.0" Trademark string "Ubuntu and Canonical are
+ registered trademarks of Canonical Ltd." (Rg, It, Bd, BdIt) DM
+ (LP: #677450)
+ * (Design) Check aligning of hyphen, math signs em, en, check braces
+ and other brackets. 16/11 (LP: #676465)
+ * (Production) Pixel per em indicator added at U+F000 (Rg, It, Bd,
+ BdIt) (LP: #615787)
+ * (Production) Version number indicator added at U+EFFD (Rg, It, Bd,
+ BdIt) (LP: #640623)
+ * (Production) fstype bit set to 0 - Editable (Rg, It, Bd, BdIt)
+ (LP: #648406)
+
+2010-10-05 (Paul Sladen) Ubuntu Font Family version 0.69
+
+ [Dalton Maag]
+ * Italic,
+ - Hinting on lowercase Italic l amended 19ppm (LP: #632451)
+ - Hinting on lowercase Italic u amended 12ppm (LP: #626376)
+
+ * Regular, Italic, Bold, BoldItalic
+ - New Rupee Sign added @ U+20B9 (LP: #645987)
+ - Ubuntu Roundel added @ U+E0FF (LP: #651606)
+
+ [Paul Sladen]
+ * All
+ - Removed "!ubu" GSUB.calt ligature for U+E0FF (LP: #651606)
+
+
+Acknowledgements
+
+If you make modifications be sure to add your name (N), email (E),
+web-address (if you have one) (W) and description (D). This list is in
+alphabetical order.
+
+N: Ryan Abdullah
+W: http://www.rayan.de/
+D: Arabic calligraphy and design in collaboration with Dalton Maag
+D: Arabic testing
+
+N: Cody Boisclair
+D: Monospace low-level debugging and patching ('fixboxdrawing-ft.py')
+
+N: Amélie Bonet
+W: http://ameliebonet.com/
+D: Type design with Dalton Maag, particularly Ubuntu Mono and Ubuntu Condensed
+
+N: Jason Campbell
+W: http://www.campbellgraphics.com/design/fonts.shtml
+D: Monospace hinting (first phase) at Dalton Maag
+
+N: Pilar Cano
+W: http://www.pilarcano.com/
+D: Hebrew realisation with Dalton Maag
+
+N: Fernando Caro
+D: Type design with Dalton Maag, particularly Ubuntu Condensed
+
+N: Ron Carpenter
+W: http://www.daltonmaag.com/
+D: Type design with Dalton Maag
+D: Arabic realisation in collaboration with Ryan Abdullah
+
+N: Vincent Connare
+W: http://www.connare.com/
+D: Type design, and engineering with Dalton Maag
+D: Monospace hinting (second phase) at Dalton Maag
+
+N: Dave Crossland
+E: dave@understandingfonts.com
+W: http://understandingfonts.com/
+D: Documentation and libre licensing guidance
+D: Google Webfont integration at Google
+
+N: Steve Edwards
+W: http://www.madebymake.com/
+D: font.ubuntu.com revamp implementation with Canonical Web Team
+
+N: Iain Farrell
+W: http://www.flickr.com/photos/iain
+D: Ubuntu Font Family delivery for the Ubuntu UX team at Canonical
+
+N: Marc Foley
+W: http://www.marcfoley.co/
+D: Font Engineer at Dalton Maag for the 2015 updates
+
+N: Shiraaz Gabru
+W: http://www.daltonmaag.com/
+D: Ubuntu Font Family project management at Dalton Maag
+
+N: Marcus Haslam
+W: http://design.canonical.com/author/marcus-haslam/
+D: Creative inspiration
+
+N: Ben Laenen
+D: Inspiration behind the pixels-per-em (PPEM) readout debugging glyph at U+F000
+ (for this font the concept was re-implemented from scratch by Dalton-Maag)
+
+N: Bruno Maag
+W: http://www.daltonmaag.com/
+D: Stylistic direction of the Ubuntu Font Family, as head of Dalton Maag
+
+N: Ivanka Majic
+W: http://www.ivankamajic.com/
+D: Guiding the UX team and Cyrillic feedback
+
+N: David Marshall
+W: http://www.daltonmaag.com/
+D: Technical guidance and administration at Dalton Maag
+
+N: Malcolm Wooden
+W: http://www.daltonmaag.com/
+D: Font Engineering at Dalton Maag
+
+N: Lukas Paltram
+W: http://www.daltonmaag.com/
+D: Type design with Dalton Maag
+
+N: Júlio Reis
+D: Date fixes to the documentation
+
+N: Rodrigo Rivas
+D: Indian Rupee Sign glyph
+
+N: Mark Shuttleworth
+E: mark@ubuntu.com
+W: http://www.markshuttleworth.com/
+D: Executive quality-control and funding
+
+N: Paul Sladen
+E: ubuntu@paul.sladen.org
+W: http://www.paul.sladen.org/
+D: Bug triaging, packaging at Ubuntu and Canonical
+
+N: Nicolas Spalinger
+W: http://planet.open-fonts.org
+D: Continuous guidance on libre/open font licensing, best practises in source
+ tree layout, release and packaging (pkg-fonts Debian team)
+
+N: Kenneth Wimer
+D: Initial PPA packaging
+
+* Canonical Ltd is the primary commercial sponsor of the Ubuntu and
+ Kubuntu operating systems
+* Dalton Maag are a custom type foundry headed by Bruno Maag
+
+For further documentation, information on contributors, source code
+downloads and those involved with the Ubuntu Font Family, visit:
+
+ http://font.ubuntu.com/
diff --git a/src/com/proxy/kiwi/res/fonts/LICENCE-FAQ.txt b/src/com/proxy/kiwi/res/fonts/LICENCE-FAQ.txt
new file mode 100644
index 0000000..776a25e
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/LICENCE-FAQ.txt
@@ -0,0 +1,177 @@
+ Ubuntu Font Family Licensing FAQ
+
+ Stylistic Foundations
+
+ The Ubuntu Font Family is the first time that a libre typeface has been
+ designed professionally and explicitly with the intent of developing a
+ public and long-term community-based development process.
+
+ When developing an open project, it is generally necessary to have firm
+ foundations: a font needs to maintain harmony within itself even across
+ many type designers and writing systems. For the [1]Ubuntu Font Family,
+ the process has been guided with the type foundry Dalton Maag setting
+ the project up with firm stylistic foundation covering several
+ left-to-right scripts: Latin, Greek and Cyrillic; and right-to-left
+ scripts: Arabic and Hebrew (due in 2011).
+
+ With this starting point the community will, under the supervision of
+ [2]Canonical and [3]Dalton Maag, be able to build on the existing font
+ sources to expand their character coverage. Ultimately everybody will
+ be able to use the Ubuntu Font Family in their own written languages
+ across the whole of Unicode (and this will take some time!).
+
+ Licensing
+
+ The licence chosen by any free software project is one of the
+ foundational decisions that sets out how derivatives and contributions
+ can occur, and in turn what kind of community will form around the
+ project.
+
+ Using a licence that is compatible with other popular licences is a
+ powerful constraint because of the [4]network effects: the freedom to
+ share improvements between projects allows free software to reach
+ high-quality over time. Licence-proliferation leads to many
+ incompatible licences, undermining the network effect, the freedom to
+ share and ultimately making the libre movement that Ubuntu is a part of
+ less effective. For all kinds of software, writing a new licence is not
+ to be taken lightly and is a choice that needs to be thoroughly
+ justified if this path is taken.
+
+ Today it is not clear to Canonical what the best licence for a font
+ project like the Ubuntu Font Family is: one that starts life designed
+ by professionals and continues with the full range of community
+ development, from highly commercial work in new directions to curious
+ beginners' experimental contributions. The fast and steady pace of the
+ Ubuntu release cycle means that an interim libre licence has been
+ necessary to enable the consideration of the font family as part of
+ Ubuntu 10.10 operating system release.
+
+ Before taking any decision on licensing, Canonical as sponsor and
+ backer of the project has reviewed the many existing licenses used for
+ libre/open fonts and engaged the stewards of the most popular licenses
+ in detailed discussions. The current interim licence is the first step
+ in progressing the state-of-the-art in licensing for libre/open font
+ development.
+
+ The public discussion must now involve everyone in the (comparatively
+ new) area of the libre/open font community; including font users,
+ software freedom advocates, open source supporters and existing libre
+ font developers. Most importantly, the minds and wishes of professional
+ type designers considering entering the free software business
+ community must be taken on board.
+
+ Conversations and discussion has taken place, privately, with
+ individuals from the following groups (generally speaking personally on
+ behalf of themselves, rather than their affiliations):
+ * [5]SIL International
+ * [6]Open Font Library
+ * [7]Software Freedom Law Center
+ * [8]Google Font API
+
+ Document embedding
+
+ One issue highlighted early on in the survey of existing font licences
+ is that of document embedding. Almost all font licences, both free and
+ unfree, permit embedding a font into a document to a certain degree.
+ Embedding a font with other works that make up a document creates a
+ "combined work" and copyleft would normally require the whole document
+ to be distributed under the terms of the font licence. As beautiful as
+ the font might be, such a licence makes a font too restrictive for
+ useful general purpose digital publishing.
+
+ The situation is not entirely unique to fonts and is encountered also
+ with tools such as GNU Bison: a vanilla GNU GPL licence would require
+ anything generated with Bison to be made available under the terms of
+ the GPL as well. To avoid this, Bison is [9]published with an
+ additional permission to the GPL which allows the output of Bison to be
+ made available under any licence.
+
+ The conflict between licensing of fonts and licensing of documents, is
+ addressed in two popular libre font licences, the SIL OFL and GNU GPL:
+ * [10]SIL Open Font Licence: When OFL fonts are embedded in a
+ document, the OFL's terms do not apply to that document. (See
+ [11]OFL-FAQ for details.
+ * [12]GPL Font Exception: The situation is resolved by granting an
+ additional permission to allow documents to not be covered by the
+ GPL. (The exception is being reviewed).
+
+ The Ubuntu Font Family must also resolve this conflict, ensuring that
+ if the font is embedded and then extracted it is once again clearly
+ under the terms of its libre licence.
+
+ Long-term licensing
+
+ Those individuals involved, especially from Ubuntu and Canonical, are
+ interested in finding a long-term libre licence that finds broad favour
+ across the whole libre/open font community. The deliberation during the
+ past months has been on how to licence the Ubuntu Font Family in the
+ short-term, while knowingly encouraging everyone to pursue a long-term
+ goal.
+ * [13]Copyright assignment will be required so that the Ubuntu Font
+ Family's licensing can be progressively expanded to one (or more)
+ licences, as best practice continues to evolve within the
+ libre/open font community.
+ * Canonical will support and fund legal work on libre font licensing.
+ It is recognised that the cost and time commitments required are
+ likely to be significant. We invite other capable parties to join
+ in supporting this activity.
+
+ The GPL version 3 (GPLv3) will be used for Ubuntu Font Family build
+ scripts and the CC-BY-SA for associated documentation and non-font
+ content: all items which do not end up embedded in general works and
+ documents.
+
+Ubuntu Font Licence
+
+ For the short-term only, the initial licence is the [14]Ubuntu Font
+ License (UFL). This is loosely inspired from the work on the SIL
+ OFL 1.1, and seeks to clarify the issues that arose during discussions
+ and legal review, from the perspective of the backers, Canonical Ltd.
+ Those already using established licensing models such as the GPL, OFL
+ or Creative Commons licensing should have no worries about continuing
+ to use them. The Ubuntu Font Licence (UFL) and the SIL Open Font
+ Licence (SIL OFL) are not identical and should not be confused with
+ each other. Please read the terms precisely. The UFL is only intended
+ as an interim license, and the overriding aim is to support the
+ creation of a more suitable and generic libre font licence. As soon as
+ such a licence is developed, the Ubuntu Font Family will migrate to
+ it—made possible by copyright assignment in the interium. Between the
+ OFL 1.1, and the UFL 1.0, the following changes are made to produce the
+ Ubuntu Font Licence:
+ * Clarification:
+
+ 1. Document embedding (see [15]embedding section above).
+ 2. Apply at point of distribution, instead of receipt
+ 3. Author vs. copyright holder disambiguation (type designers are
+ authors, with the copyright holder normally being the funder)
+ 4. Define "Propagate" (for internationalisation, similar to the GPLv3)
+ 5. Define "Substantially Changed"
+ 6. Trademarks are explicitly not transferred
+ 7. Refine renaming requirement
+
+ Streamlining:
+ 8. Remove "not to be sold separately" clause
+ 9. Remove "Reserved Font Name(s)" declaration
+
+ A visual demonstration of how these points were implemented can be
+ found in the accompanying coloured diff between SIL OFL 1.1 and the
+ Ubuntu Font Licence 1.0: [16]ofl-1.1-ufl-1.0.diff.html
+
+References
+
+ 1. http://font.ubuntu.com/
+ 2. http://www.canonical.com/
+ 3. http://www.daltonmaag.com/
+ 4. http://en.wikipedia.org/wiki/Network_effect
+ 5. http://scripts.sil.org/
+ 6. http://openfontlibrary.org/
+ 7. http://www.softwarefreedom.org/
+ 8. http://code.google.com/webfonts
+ 9. http://www.gnu.org/licenses/gpl-faq.html#CanIUseGPLToolsForNF
+ 10. http://scripts.sil.org/OFL_web
+ 11. http://scripts.sil.org/OFL-FAQ_web
+ 12. http://www.gnu.org/licenses/gpl-faq.html#FontException
+ 13. https://launchpad.net/~uff-contributors
+ 14. http://font.ubuntu.com/ufl/ubuntu-font-licence-1.0.txt
+ 15. http://font.ubuntu.com/ufl/FAQ.html#embedding
+ 16. http://font.ubuntu.com/ufl/ofl-1.1-ufl-1.0.diff.html
diff --git a/src/com/proxy/kiwi/res/fonts/LICENCE.txt b/src/com/proxy/kiwi/res/fonts/LICENCE.txt
new file mode 100644
index 0000000..ae78a8f
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/LICENCE.txt
@@ -0,0 +1,96 @@
+-------------------------------
+UBUNTU FONT LICENCE Version 1.0
+-------------------------------
+
+PREAMBLE
+This licence allows the licensed fonts to be used, studied, modified and
+redistributed freely. The fonts, including any derivative works, can be
+bundled, embedded, and redistributed provided the terms of this licence
+are met. The fonts and derivatives, however, cannot be released under
+any other licence. The requirement for fonts to remain under this
+licence does not require any document created using the fonts or their
+derivatives to be published under this licence, as long as the primary
+purpose of the document is not to be a vehicle for the distribution of
+the fonts.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this licence and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Original Version" refers to the collection of Font Software components
+as received under this licence.
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to
+a new environment.
+
+"Copyright Holder(s)" refers to all individuals and companies who have a
+copyright ownership of the Font Software.
+
+"Substantially Changed" refers to Modified Versions which can be easily
+identified as dissimilar to the Font Software by users of the Font
+Software comparing the Original Version with the Modified Version.
+
+To "Propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification and with or without charging
+a redistribution fee), making available to the public, and in some
+countries other activities as well.
+
+PERMISSION & CONDITIONS
+This licence does not grant any rights under trademark law and all such
+rights are reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to propagate the Font Software, subject to
+the below conditions:
+
+1) Each copy of the Font Software must contain the above copyright
+notice and this licence. These can be included either as stand-alone
+text files, human-readable headers or in the appropriate machine-
+readable metadata fields within text or binary files as long as those
+fields can be easily viewed by the user.
+
+2) The font name complies with the following:
+(a) The Original Version must retain its name, unmodified.
+(b) Modified Versions which are Substantially Changed must be renamed to
+avoid use of the name of the Original Version or similar names entirely.
+(c) Modified Versions which are not Substantially Changed must be
+renamed to both (i) retain the name of the Original Version and (ii) add
+additional naming elements to distinguish the Modified Version from the
+Original Version. The name of such Modified Versions must be the name of
+the Original Version, with "derivative X" where X represents the name of
+the new work, appended to that name.
+
+3) The name(s) of the Copyright Holder(s) and any contributor to the
+Font Software shall not be used to promote, endorse or advertise any
+Modified Version, except (i) as required by this licence, (ii) to
+acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
+their explicit written permission.
+
+4) The Font Software, modified or unmodified, in part or in whole, must
+be distributed entirely under this licence, and must not be distributed
+under any other licence. The requirement for fonts to remain under this
+licence does not affect any document created using the Font Software,
+except any version of the Font Software extracted from a document
+created using the Font Software may only be distributed under this
+licence.
+
+TERMINATION
+This licence becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/com/proxy/kiwi/res/fonts/README.txt b/src/com/proxy/kiwi/res/fonts/README.txt
new file mode 100644
index 0000000..5602821
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/README.txt
@@ -0,0 +1,16 @@
+ ----------------------
+ Ubuntu Font Family
+ ======================
+
+The Ubuntu Font Family are a set of matching new libre/open fonts in
+development during 2010--2011. And with further expansion work and
+bug fixing during 2015. The development is being funded by
+Canonical Ltd on behalf the wider Free Software community and the
+Ubuntu project. The technical font design work and implementation is
+being undertaken by Dalton Maag.
+
+Both the final font Truetype/OpenType files and the design files used
+to produce the font family are distributed under an open licence and
+you are expressly encouraged to experiment, modify, share and improve.
+
+ http://font.ubuntu.com/
diff --git a/src/com/proxy/kiwi/res/fonts/TRADEMARKS.txt b/src/com/proxy/kiwi/res/fonts/TRADEMARKS.txt
new file mode 100644
index 0000000..d34265b
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/TRADEMARKS.txt
@@ -0,0 +1,4 @@
+Ubuntu and Canonical are registered trademarks of Canonical Ltd.
+
+The licence accompanying these works does not grant any rights
+under trademark law and all such rights are reserved.
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-B.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-B.ttf
new file mode 100644
index 0000000..b173da2
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-B.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-BI.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-BI.ttf
new file mode 100644
index 0000000..72a5a99
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-BI.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-C.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-C.ttf
new file mode 100644
index 0000000..602a3ee
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-C.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-L.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-L.ttf
new file mode 100644
index 0000000..ed0f5bc
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-L.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-LI.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-LI.ttf
new file mode 100644
index 0000000..c6cec55
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-LI.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-M.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-M.ttf
new file mode 100644
index 0000000..ca9c03a
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-M.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-MI.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-MI.ttf
new file mode 100644
index 0000000..e8d186c
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-MI.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-R.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-R.ttf
new file mode 100644
index 0000000..d748728
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-R.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/Ubuntu-RI.ttf b/src/com/proxy/kiwi/res/fonts/Ubuntu-RI.ttf
new file mode 100644
index 0000000..4f2d2bc
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/Ubuntu-RI.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/UbuntuMono-B.ttf b/src/com/proxy/kiwi/res/fonts/UbuntuMono-B.ttf
new file mode 100644
index 0000000..7bd6665
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/UbuntuMono-B.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/UbuntuMono-BI.ttf b/src/com/proxy/kiwi/res/fonts/UbuntuMono-BI.ttf
new file mode 100644
index 0000000..6c5b8ba
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/UbuntuMono-BI.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/UbuntuMono-R.ttf b/src/com/proxy/kiwi/res/fonts/UbuntuMono-R.ttf
new file mode 100644
index 0000000..fdd309d
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/UbuntuMono-R.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/UbuntuMono-RI.ttf b/src/com/proxy/kiwi/res/fonts/UbuntuMono-RI.ttf
new file mode 100644
index 0000000..18f81a2
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/UbuntuMono-RI.ttf differ
diff --git a/src/com/proxy/kiwi/res/fonts/copyright.txt b/src/com/proxy/kiwi/res/fonts/copyright.txt
new file mode 100644
index 0000000..7734070
--- /dev/null
+++ b/src/com/proxy/kiwi/res/fonts/copyright.txt
@@ -0,0 +1,5 @@
+Copyright 2010,2011 Canonical Ltd.
+
+This Font Software is licensed under the Ubuntu Font Licence, Version
+1.0. https://launchpad.net/ubuntu-font-licence
+
diff --git a/src/com/proxy/kiwi/res/fonts/fontawesome.ttf b/src/com/proxy/kiwi/res/fonts/fontawesome.ttf
new file mode 100644
index 0000000..26dea79
Binary files /dev/null and b/src/com/proxy/kiwi/res/fonts/fontawesome.ttf differ
diff --git a/src/com/proxy/kiwi/res/item.fxml b/src/com/proxy/kiwi/res/item.fxml
new file mode 100644
index 0000000..50814b4
--- /dev/null
+++ b/src/com/proxy/kiwi/res/item.fxml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/proxy/kiwi/res/kiwi.png b/src/com/proxy/kiwi/res/kiwi.png
new file mode 100644
index 0000000..dee1711
Binary files /dev/null and b/src/com/proxy/kiwi/res/kiwi.png differ
diff --git a/src/com/proxy/kiwi/res/kiwi.psd b/src/com/proxy/kiwi/res/kiwi.psd
new file mode 100644
index 0000000..7c3183c
Binary files /dev/null and b/src/com/proxy/kiwi/res/kiwi.psd differ
diff --git a/src/com/proxy/kiwi/res/kiwi_small.png b/src/com/proxy/kiwi/res/kiwi_small.png
new file mode 100644
index 0000000..88000ec
Binary files /dev/null and b/src/com/proxy/kiwi/res/kiwi_small.png differ
diff --git a/src/com/proxy/kiwi/res/loading.fxml b/src/com/proxy/kiwi/res/loading.fxml
new file mode 100644
index 0000000..2d438ba
--- /dev/null
+++ b/src/com/proxy/kiwi/res/loading.fxml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/proxy/kiwi/res/loading.gif b/src/com/proxy/kiwi/res/loading.gif
new file mode 100644
index 0000000..cbe59fb
Binary files /dev/null and b/src/com/proxy/kiwi/res/loading.gif differ
diff --git a/src/com/proxy/kiwi/res/options.png b/src/com/proxy/kiwi/res/options.png
new file mode 100644
index 0000000..041ac6b
Binary files /dev/null and b/src/com/proxy/kiwi/res/options.png differ
diff --git a/src/com/proxy/kiwi/res/reading_pane.fxml b/src/com/proxy/kiwi/res/reading_pane.fxml
new file mode 100644
index 0000000..0da9f73
--- /dev/null
+++ b/src/com/proxy/kiwi/res/reading_pane.fxml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/proxy/kiwi/res/search.png b/src/com/proxy/kiwi/res/search.png
new file mode 100644
index 0000000..877649f
Binary files /dev/null and b/src/com/proxy/kiwi/res/search.png differ
diff --git a/src/com/proxy/kiwi/res/search_box.fxml b/src/com/proxy/kiwi/res/search_box.fxml
new file mode 100644
index 0000000..7f405cd
--- /dev/null
+++ b/src/com/proxy/kiwi/res/search_box.fxml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file