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 + 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 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 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 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/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