merge android branch (specs)
authorNiki Roo <niki@nikiroo.be>
Sat, 18 May 2019 16:20:34 +0000 (18:20 +0200)
committerNiki Roo <niki@nikiroo.be>
Sat, 18 May 2019 16:20:34 +0000 (18:20 +0200)
91 files changed:
Makefile.base
README-fr.md
README.md
TODO.md
android/app/build.gradle.base [deleted file]
android/app/proguard-rules.pro [deleted file]
android/build.gradle [deleted file]
android/gradle.properties [deleted file]
android/gradle/wrapper/gradle-wrapper.jar [deleted file]
android/gradle/wrapper/gradle-wrapper.properties [deleted file]
android/gradlew [deleted file]
android/gradlew.bat [deleted file]
android/settings.gradle [deleted file]
changelog-fr.md
changelog.md
configure.sh
fanfix.sysv
libs/licenses/jexer-0.0.4_LICENSE.txt [moved from libs/jexer-0.0.4_LICENSE.txt with 100% similarity]
libs/licenses/unbescape-1.1.4_LICENSE.txt [moved from libs/unbescape-1.1.4_LICENSE.txt with 100% similarity]
libs/nikiroo-utils-4.5.2-sources.jar [deleted file]
libs/nikiroo-utils-4.7.2-dev-sources.jar [new file with mode: 0644]
src/AndroidManifest.xml [deleted file]
src/be/nikiroo/fanfix/DataLoader.java
src/be/nikiroo/fanfix/Instance.java
src/be/nikiroo/fanfix/Main.java
src/be/nikiroo/fanfix/VersionCheck.java
src/be/nikiroo/fanfix/bundles/Config.java
src/be/nikiroo/fanfix/bundles/StringIdGui.java
src/be/nikiroo/fanfix/bundles/config.properties
src/be/nikiroo/fanfix/bundles/resources_core.properties
src/be/nikiroo/fanfix/bundles/resources_core_fr.properties
src/be/nikiroo/fanfix/bundles/resources_gui.properties
src/be/nikiroo/fanfix/bundles/resources_gui_fr.properties
src/be/nikiroo/fanfix/data/Chapter.java
src/be/nikiroo/fanfix/data/MetaData.java
src/be/nikiroo/fanfix/data/Paragraph.java
src/be/nikiroo/fanfix/data/Story.java
src/be/nikiroo/fanfix/data/package-info.java
src/be/nikiroo/fanfix/library/BasicLibrary.java
src/be/nikiroo/fanfix/library/CacheLibrary.java
src/be/nikiroo/fanfix/library/LocalLibrary.java
src/be/nikiroo/fanfix/library/RemoteLibrary.java
src/be/nikiroo/fanfix/library/RemoteLibraryException.java [new file with mode: 0644]
src/be/nikiroo/fanfix/library/RemoteLibraryServer.java
src/be/nikiroo/fanfix/package-info.java
src/be/nikiroo/fanfix/reader/BasicReader.java
src/be/nikiroo/fanfix/reader/Reader.java
src/be/nikiroo/fanfix/reader/android/AndroidReader.java [deleted file]
src/be/nikiroo/fanfix/reader/android/AndroidReaderActivity.java [deleted file]
src/be/nikiroo/fanfix/reader/android/AndroidReaderBook.java [deleted file]
src/be/nikiroo/fanfix/reader/android/AndroidReaderGroup.java [deleted file]
src/be/nikiroo/fanfix/reader/cli/CliReader.java
src/be/nikiroo/fanfix/reader/tui/TuiReader.java
src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java
src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java
src/be/nikiroo/fanfix/reader/ui/GuiReader.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderBookInfo.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderCoverImager.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderFrame.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderGroup.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderMainPanel.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderNavBar.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchAction.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByNamePanel.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchFrame.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderViewer.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderViewerPanel.java
src/be/nikiroo/fanfix/searchable/BasicSearchable.java [new file with mode: 0644]
src/be/nikiroo/fanfix/searchable/Fanfiction.java [new file with mode: 0644]
src/be/nikiroo/fanfix/searchable/MangaLel.java [new file with mode: 0644]
src/be/nikiroo/fanfix/searchable/SearchableTag.java [new file with mode: 0644]
src/be/nikiroo/fanfix/supported/BasicSupport.java
src/be/nikiroo/fanfix/supported/BasicSupport_Deprecated.java
src/be/nikiroo/fanfix/supported/Cbz.java
src/be/nikiroo/fanfix/supported/E621.java
src/be/nikiroo/fanfix/supported/EHentai.java
src/be/nikiroo/fanfix/supported/Epub.java
src/be/nikiroo/fanfix/supported/Fanfiction.java
src/be/nikiroo/fanfix/supported/Fimfiction.java
src/be/nikiroo/fanfix/supported/FimfictionApi.java
src/be/nikiroo/fanfix/supported/Html.java
src/be/nikiroo/fanfix/supported/InfoReader.java
src/be/nikiroo/fanfix/supported/InfoText.java
src/be/nikiroo/fanfix/supported/MangaFox.java
src/be/nikiroo/fanfix/supported/MangaLel.java
src/be/nikiroo/fanfix/supported/SupportType.java
src/be/nikiroo/fanfix/supported/Text.java
src/be/nikiroo/fanfix/supported/YiffStar.java
src/be/nikiroo/fanfix/test/BasicSupportTest.java

index 586c38e377fa9b0016081361d1c3d336b57ab5f2..0d365b8448b1c4d7f4bda91c0aca3ff1b70307f8 100644 (file)
@@ -6,6 +6,9 @@
 # - 1.2.0: add 'apk'
 # - 1.2.1: improve 'apk' and add 'android'
 # - 1.3.0: add 'man' for man(ual) pages
+# - 1.4.0: remove android stuff (not working anyway)
+# - 1.5.0: include sources and readme/changelog in jar
+# - 1.5.1: include binaries from libs/bin/ into the jar
 
 # Required parameters (the commented out ones are supposed to be per project):
 
@@ -18,9 +21,6 @@
 #SJAR_FLAGS += a list of things to pack, each usually prefixed with "-C src/",
 #              for *-sources.jar files
 #TEST_PARAMS = any parameter to pass to the test runnable when "test-run"
-#ID_FOR_ANDROID = id of activity to launch for Android
-#RM_FOR_ANDROID = packages (if it ends with /) or classes to ignore for APK 
-#              generation
 
 JAVAC = javac
 JAVAC_FLAGS += -encoding UTF-8 -d ./bin/ -cp ./src/
@@ -48,12 +48,10 @@ help:
        @echo " make run-test   : to run the test program from the binaries"
        @echo " make jrun       : to run the program from the jar file"
        @echo " make install    : to install the application into $$PREFIX"
-       @echo " make android    : to prepare the sources in android/ for Studio"
-       @echo " make apk        : to compile the APK file"
        @echo " make ifman      : to make the manual pages (if pandoc is found)"
        @echo " make man        : to make the manual pages (requires pandoc)"
 
-.PHONY: all clean mrproper mrpropre build run jrun jar sjar resources test-resources install libs love apk android ifman man
+.PHONY: all clean mrproper mrpropre build run jrun jar sjar resources test-resources install libs ifman man love
 
 bin:
        @mkdir -p bin
@@ -83,8 +81,6 @@ test: test-resources
 
 clean:
        rm -rf bin/
-       rm -rf android/.gradle android/build android/app/build android/app/build.gradle
-       [ ! -L android/app/src/main/java ] || rm -rf android/app/src
        @echo Removing sources taken from libs...
        @for lib in libs/*-sources.jar libs/*-sources.patch.jar; do \
                if [ "$$lib" != 'libs/*-sources.jar' -a "$$lib" != 'libs/*-sources.patch.jar' ]; then \
@@ -107,8 +103,6 @@ mrpropre: clean
        rm -f $(NAME)-debug.apk
        [ ! -e VERSION ] || rm -f "$(NAME)-`cat VERSION`.jar"
        [ ! -e VERSION ] || rm -f "$(NAME)-`cat VERSION`-sources.jar"
-       [ ! -e VERSION ] || rm -f "$(NAME)-`cat VERSION`.apk"
-       [ ! -e VERSION ] || rm -f "$(NAME)-`cat VERSION`-debug.apk"
 
 love:
        @echo " ...not war."
@@ -145,19 +139,29 @@ libs: bin
        @[ ! -d libs ] || touch bin/libs
 
 $(NAME)-sources.jar: libs
+       @ls *.md >/dev/null || cp VERSION README.md
        @echo Making sources JAR file...
        @echo > bin/manifest
-       @[ "$(SJAR_FLAGS)" = "" ] || echo Creating $(NAME)-sources.jar...
-       @[ "$(SJAR_FLAGS)" = "" ] || $(JAR) cfm $(NAME)-sources.jar bin/manifest $(SJAR_FLAGS)
-       @[ "$(SJAR_FLAGS)" = "" ] || [ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`-sources.jar"...
-       @[ "$(SJAR_FLAGS)" = "" ] || [ ! -e VERSION ] || cp $(NAME)-sources.jar "$(NAME)-`cat VERSION`-sources.jar"
+       @[ "$(SJAR_FLAGS)" != "" ] || echo No sources JAR file defined, skipping
+       @[ "$(SJAR_FLAGS)"  = "" ] || echo Creating $(NAME)-sources.jar...
+       @[ "$(SJAR_FLAGS)"  = "" ] || $(JAR) cfm $(NAME)-sources.jar bin/manifest -C ./ *.md $(SJAR_FLAGS)
+       @[ "$(SJAR_FLAGS)"  = "" ] || [ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`-sources.jar"...
+       @[ "$(SJAR_FLAGS)"  = "" ] || [ ! -e VERSION ] || cp $(NAME)-sources.jar "$(NAME)-`cat VERSION`-sources.jar"
 
 $(NAME).jar: resources
        @[ -e bin/$(MAIN).class ] || echo You need to build the sources
        @[ -e bin/$(MAIN).class ]
+       @ls *.md >/dev/null || cp VERSION README.md
+       @echo "Copying documentation into bin/..."
+       @cp -r *.md bin/ || cp VERSION bin/no-documentation.md
+       @[ ! -d libs/bin/ ] || echo "Copying additional binaries from libs/bin/ into bin/..."
+       @[ ! -d libs/bin/ ] || cp -r libs/bin/* bin/
+       @echo "Copying sources into bin/..."
+       @cp -r src/* bin/
+       @echo "Making jar..."
        @echo "Main-Class: `echo "$(MAIN)" | sed 's:/:.:g'`" > bin/manifest
        @echo >> bin/manifest
-       $(JAR) cfm $(NAME).jar bin/manifest $(JAR_FLAGS)
+       $(JAR) cfm $(NAME).jar bin/manifest -C ./ *.md $(JAR_FLAGS)
        @[ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`.jar"...
        @[ ! -e VERSION ] || cp $(NAME).jar "$(NAME)-`cat VERSION`.jar"
 
@@ -192,58 +196,6 @@ install:
                cp -r man/ "$(PREFIX)"/share/; \
        fi
 
-android: android/app/src
-
-android/app/src:
-       @[ -d android ] || echo No android/ directory found
-       @[ -d android ]
-       @[ -e android/local.properties ] || echo 'You need to create android/local.properties and add "sdk.dir=PATH_TO_SDK"'
-       @[ -e android/local.properties ]
-       @mkdir -p android/app/src/main
-       @echo Linking sources...
-       @( \
-               cd android/app/src/main; \
-               ln -s ../../../../src/AndroidManifest.xml .; \
-               ln -s ../../../../res .; \
-               ln -s ../../../../src ./java; \
-       )
-       @echo Fixing configuration...
-       @( \
-               cd android/app/src/main/java; \
-               excl="\\n";\
-               if [ "${RM_FOR_ANDROID}" != "" ]; then \
-                       echo Ignoring uneeded sources...; \
-                       for file in ${RM_FOR_ANDROID}; do \
-                               excl="$${excl}exclude '**/$${file}'\\n";\
-                       done; \
-               fi; \
-               cd ../../../ ; \
-               cat build.gradle.base \
-                       | sed 's:\(applicationId "\)":\1${ID_FOR_ANDROID}":' \
-                       | sed "s:\s*exclude '':$$excl:g" \
-               > build.gradle; \
-       )
-
-apk: libs ${NAME}.apk
-       @echo Building APK files...
-
-${NAME}.apk: ${NAME}-debug.apk
-
-${NAME}-debug.apk: android
-       @echo Starting gradlew assemble...
-       @( \
-               cd android/; \
-               bash gradlew assemble && ( \
-                       cd ..; \
-                       cp android/app/build/outputs/apk/release/app-release-unsigned.apk ${NAME}.apk; \
-                       cp android/app/build/outputs/apk/debug/app-debug.apk ${NAME}-debug.apk; \
-                       [ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`.apk"...; \
-                       [ ! -e VERSION ] || cp $(NAME).apk "$(NAME)-`cat VERSION`.apk"; \
-                       [ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`-debug.apk"...; \
-                       [ ! -e VERSION ] || cp $(NAME).apk "$(NAME)-`cat VERSION`-debug.apk"; \
-               ); \
-       )
-
 ifman:
        @if pandoc -v >/dev/null 2>&1; then \
                make man; \
index cb76040b15f8c20c58677486cb288407c1673811..5a0d7f8dadda047e57d5ae30a3000fceb216f903 100644 (file)
@@ -10,6 +10,10 @@ Fanfix est un petit programme Java qui peut télécharger des histoires sur inte
 - ```fanfix``` --convert [*URL*] [*output_type*] [*target*] (+info)
 - ```fanfix``` --read [*id*] ([*chapter number*])
 - ```fanfix``` --read-url [*URL*] ([*chapter number*])
+- ```fanfix``` --search
+- ```fanfix``` --search [*where*] [*keywords*] (page [*page*]) (item [*item*])
+- ```fanfix``` --search-tag
+- ```fanfix``` --search-tag [*index 1*]... (page [*page*]) (item [*item*])
 - ```fanfix``` --list
 - ```fanfix``` --set-reader [*GUI* | *TUI* | *CLI*]
 - ```fanfix``` --server [*key*] [*port*]
@@ -46,7 +50,7 @@ Pour le moment, les sites suivants sont supportés :
 - https://e621.net/ : un site Furry proposant des comics, y compris de MLP
 - https://sofurry.com/ : même chose, mais orienté sur les histoires plutôt que les images
 - https://e-hentai.org/ : support ajouté sur demande : n'hésitez pas à demander un site !
-- https://www.manga-lel.com/ : un site proposant beaucoup de mangas, en français
+- http://mangas-lecture-en-ligne.fr/ : un site proposant beaucoup de mangas, en français
 
 ### Types de fichiers supportés
 
@@ -85,6 +89,10 @@ Les arguments suivants sont aussi supportés :
 - ```--convert [URL] [output_type] [target] (+info)```: convertir l'histoire vers le fichier donné, et forcer l'ajout d'un fichier .info si +info est utilisé
 - ```--read [id] ([chapter number])```: afficher l'histoire "id"
 - ```--read-url [URL] ([chapter number])```: convertir l'histoire et la lire à la volée, sans la sauver
+- ```--search```: liste les sites supportés (```where```)
+- ```--search [where] [keywords] (page [page]) (item [item])```: lance une recherche et affiche les résultats de la page ```page``` (page 1 par défaut), et de l'item ```item``` spécifique si demandé
+- ```--tag [where]```: liste tous les tags supportés par ce site web
+- ```--tag [index 1]... (page [page]) (item [item])```: affine la recherche, tag par tag, et affiche si besoin les sous-tags, les histoires ou les infos précises de l'histoire demandée
 - ```--list```: lister les histoires presentes dans la librairie et leurs IDs
 - ```--set-reader [reader type]```: changer le type de lecteur pour la commande en cours sur CLI, TUI ou GUI
 - ```--server [key] [port]```: démarrer un serveur d'histoires sur ce port
index 22e0b60f40342ab5f2b472c4df324ad6a550aa20..7e7c3b9e22e8ad15648b99d4de2dfb76c89bfeb0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -10,6 +10,10 @@ Fanfix is a small Java program that can download stories from some supported web
 - ```fanfix``` --convert [*URL*] [*output_type*] [*target*] (+info)
 - ```fanfix``` --read [*id*] ([*chapter number*])
 - ```fanfix``` --read-url [*URL*] ([*chapter number*])
+- ```fanfix``` --search
+- ```fanfix``` --search [*where*] [*keywords*] (page [*page*]) (item [*item*])
+- ```fanfix``` --search-tag
+- ```fanfix``` --search-tag [*index 1*]... (page [*page*]) (item [*item*])
 - ```fanfix``` --list
 - ```fanfix``` --set-reader [*GUI* | *TUI* | *CLI*]
 - ```fanfix``` --server [*key*] [*port*]
@@ -46,7 +50,7 @@ Currently, the following websites are supported:
 - https://e621.net/: a Furry website supporting comics, including MLP
 - https://sofurry.com/: same thing, but story-oriented
 - https://e-hentai.org/: done upon request (so, feel free to ask for more websites!)
-- https://www.manga-lel.com/: a website offering a lot of mangas (in French)
+- http://mangas-lecture-en-ligne.fr/: a website offering a lot of mangas (in French)
 
 ### Support file types
 
@@ -85,6 +89,10 @@ The following arguments are also allowed:
 - ```--convert [URL] [output_type] [target] (+info)```: convert the story at URL into target, and force-add the .info and cover if +info is passed
 - ```--read [id] ([chapter number])```: read the given story denoted by ID from the library
 - ```--read-url [URL] ([chapter number])```: convert on the fly and read the story at URL, without saving it
+- ```--search```: list the supported websites (```where```)
+- ```--search [where] [keywords] (page [page]) (item [item])```: search on the supported website and display the given results page of stories it found, or the story details if asked
+- ```--tag [where]```: list all the tags supported by this website
+- ```--tag [index 1]... (page [page]) (item [item])```: search for the given stories or subtags, tag by tag, and display information about a specific page of results or about a specific item if requested
 - ```--list```: list the stories present in the library and their associated IDs
 - ```--set-reader [reader type]```: set the reader type to CLI, TUI or GUI for this command
 - ```--server [key] [port]```: start a story server on this port
diff --git a/TODO.md b/TODO.md
index 5b3110b4785d42ae2db95fbd5d5bf6b05662873b..f9f7692b8ad91dbcbdbc4e43eca038ab2ccd7950 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -9,6 +9,7 @@ My current planning for Fanfix (but not everything appears on this list):
     - [x] [e-Hentai](https://e-hentai.org/) requested
     - [x] Find some FR comics/manga websites
     - [ ] Find more FR thingies
+- [ ] Support videos (anime)?
 - [x] A GUI library
   - [x] Make one
   - [x] Make it run when no args passed
@@ -60,6 +61,7 @@ My current planning for Fanfix (but not everything appears on this list):
   - [ ] Sort stories by Source/Author
   - [ ] Fix UI
   - [ ] support progress events
+  - [x] give up and ask a friend...
 - [ ] Translations
   - [x] i18n system in place
   - [x] Make use of it in text
diff --git a/android/app/build.gradle.base b/android/app/build.gradle.base
deleted file mode 100644 (file)
index 4dac2b3..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-apply plugin: 'com.android.application'
-
-android {
-    compileSdkVersion 26
-    defaultConfig {
-        applicationId ""
-        minSdkVersion 14
-        targetSdkVersion 14
-        versionCode 1
-        versionName "1.0"
-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-    }
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
-        }
-    }
-       sourceSets {
-               main {
-                       java {
-                               exclude ''
-                       }
-               }
-       }
-}
-
-dependencies {
-    implementation fileTree(dir: 'libs', include: ['*.jar'])
-    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
-    implementation 'com.android.support:support-v4:26.1.0'
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'com.android.support.test:runner:1.0.1'
-    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
-}
-
-
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
deleted file mode 100644 (file)
index f1b4245..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
diff --git a/android/build.gradle b/android/build.gradle
deleted file mode 100644 (file)
index e6b32bc..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
-    
-    repositories {
-        google()
-        jcenter()
-    }
-    dependencies {
-        classpath 'com.android.tools.build:gradle:3.0.1'
-        
-
-        // NOTE: Do not place your application dependencies here; they belong
-        // in the individual module build.gradle files
-    }
-}
-
-allprojects {
-    repositories {
-        google()
-        jcenter()
-    }
-}
-
-task clean(type: Delete) {
-    delete rootProject.buildDir
-}
diff --git a/android/gradle.properties b/android/gradle.properties
deleted file mode 100644 (file)
index aac7c9b..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-# Project-wide Gradle settings.
-
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
-
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644 (file)
index 13372ae..0000000
Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644 (file)
index 6040c18..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-#Mon Dec 04 00:21:46 CET 2017
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/android/gradlew b/android/gradlew
deleted file mode 100755 (executable)
index 9d82f78..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-##  Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
-    echo "$*"
-}
-
-die ( ) {
-    echo
-    echo "$*"
-    echo
-    exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-esac
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
-    else
-        JAVACMD="$JAVA_HOME/bin/java"
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
-        fi
-        i=$((i+1))
-    done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
-    JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
deleted file mode 100644 (file)
index aec9973..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off\r
-@rem ##########################################################################\r
-@rem\r
-@rem  Gradle startup script for Windows\r
-@rem\r
-@rem ##########################################################################\r
-\r
-@rem Set local scope for the variables with windows NT shell\r
-if "%OS%"=="Windows_NT" setlocal\r
-\r
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
-set DEFAULT_JVM_OPTS=\r
-\r
-set DIRNAME=%~dp0\r
-if "%DIRNAME%" == "" set DIRNAME=.\r
-set APP_BASE_NAME=%~n0\r
-set APP_HOME=%DIRNAME%\r
-\r
-@rem Find java.exe\r
-if defined JAVA_HOME goto findJavaFromJavaHome\r
-\r
-set JAVA_EXE=java.exe\r
-%JAVA_EXE% -version >NUL 2>&1\r
-if "%ERRORLEVEL%" == "0" goto init\r
-\r
-echo.\r
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
-echo.\r
-echo Please set the JAVA_HOME variable in your environment to match the\r
-echo location of your Java installation.\r
-\r
-goto fail\r
-\r
-:findJavaFromJavaHome\r
-set JAVA_HOME=%JAVA_HOME:"=%\r
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
-\r
-if exist "%JAVA_EXE%" goto init\r
-\r
-echo.\r
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
-echo.\r
-echo Please set the JAVA_HOME variable in your environment to match the\r
-echo location of your Java installation.\r
-\r
-goto fail\r
-\r
-:init\r
-@rem Get command-line arguments, handling Windowz variants\r
-\r
-if not "%OS%" == "Windows_NT" goto win9xME_args\r
-if "%@eval[2+2]" == "4" goto 4NT_args\r
-\r
-:win9xME_args\r
-@rem Slurp the command line arguments.\r
-set CMD_LINE_ARGS=\r
-set _SKIP=2\r
-\r
-:win9xME_args_slurp\r
-if "x%~1" == "x" goto execute\r
-\r
-set CMD_LINE_ARGS=%*\r
-goto execute\r
-\r
-:4NT_args\r
-@rem Get arguments from the 4NT Shell from JP Software\r
-set CMD_LINE_ARGS=%$\r
-\r
-:execute\r
-@rem Setup the command line\r
-\r
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
-\r
-@rem Execute Gradle\r
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
-\r
-:end\r
-@rem End local scope for the variables with windows NT shell\r
-if "%ERRORLEVEL%"=="0" goto mainEnd\r
-\r
-:fail\r
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
-rem the _cmd.exe /c_ return code!\r
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
-exit /b 1\r
-\r
-:mainEnd\r
-if "%OS%"=="Windows_NT" endlocal\r
-\r
-:omega\r
diff --git a/android/settings.gradle b/android/settings.gradle
deleted file mode 100644 (file)
index e7b4def..0000000
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'
index efe992f46ea23e1d3952a02f3edae2b85f7870d9..8980fad938528dca6be498e12cdc7c195f7fd545 100644 (file)
@@ -1,5 +1,18 @@
 # Fanfix
 
+# Version WIP
+
+- new: recherche d'histoires (Fanfiction.net)
+- new: support d'un proxy
+- fix: support des CBZ contenant du texte
+- fix: correction de DEBUG=0
+- gui: correction pour le focus 
+- MangaLEL: site web changé
+- search: supporte MangaLEL
+- remote: changement du chiffrement because Google
+- remote: incompatible avec 2.x
+- remote: perfs et utilisation de la mémoire améliorées
+
 # Version 2.0.2
 
 - i18n: changer la langue dans les options fonctionne aussi quand $LANG existe
@@ -10,7 +23,7 @@
 
 # Version 2.0.1
 
-- lib: un changement de titre/source/author n'était pas toujours visible en runtime
+- core: un changement de titre/source/author n'était pas toujours visible en runtime
 - gui: ne recharger les histoires que quand nécessaire
 
 # Version 2.0.0
index 6a7256521ce097b015dccc2964acc9e014b035c4..a82e8b38d89e135b662a43a1cb56cd9b3200c59e 100644 (file)
@@ -1,5 +1,25 @@
 # Fanfix
 
+# Version WIP
+
+- new: now android-compatible (see [companion project](https://gitlab.com/Rayman22/fanfix-android))
+- new: story search (not all sources yet)
+- new: proxy support
+- fix: support hybrid CBZ (with text)
+- fix: fix DEBUG=0
+- gui: focus fix
+- gui: bg colour fix
+- gui: fix keyboard navigation support (up and down)
+- MangaLEL: website has changed
+- search: Fanfiction.net support
+- search: MangaLEL support
+- FimFictionAPI: fix NPE
+- remote: encryption mode changed because Google
+- remote: not compatible with 2.x
+- remote: now use password from config file
+- remote: worse perfs but much better memory usage
+- remote: log now includes the time of events
+
 # Version 2.0.2
 
 - i18n: setting the language in the option panel now works even with $LANG set
@@ -10,7 +30,7 @@
 
 # Version 2.0.1
 
-- lib: a change of title/source/author was not always visible at runtime
+- core: a change of title/source/author was not always visible at runtime
 - gui: only reload the stoies when needed
 
 # Version 2.0.0
index 2d3e2fecc0d8beadb1dd0ffb7b460f913b64aad7..1e12397d0e9d81e6ee3c588018d89f516209ccc3 100755 (executable)
@@ -79,10 +79,8 @@ echo "TEST = be/nikiroo/fanfix/test/Test" >> Makefile
 echo "TEST_PARAMS = $cols $ok $ko" >> Makefile
 echo "NAME = fanfix" >> Makefile
 echo "PREFIX = $PREFIX" >> Makefile
-echo "JAR_FLAGS += -C bin/ org $JCLI $JTUI $JGUI -C bin/ be -C bin/ VERSION" >> Makefile
-echo "RM_FOR_ANDROID = jexer be/nikiroo/utils/ui/* be/nikiroo/fanfix/reader/tui/* be/nikiroo/fanfix/reader/ui/*" >> Makefile
-echo "ID_FOR_ANDROID = be.nikiroo.fanfix.reader.android" >> Makefile
-#echo "SJAR_FLAGS += -C src/ org -C src/ jexer -C src/ be -C ./ LICENSE -C ./ README.md -C ./ VERSION" >> Makefile
+echo "JAR_FLAGS += -C bin/ org $JCLI $JTUI $JGUI -C bin/ be -C ./ LICENSE -C ./ VERSION -C libs/ licenses" >> Makefile
+#echo "SJAR_FLAGS += -C src/ org -C src/ jexer -C src/ be -C ./ LICENSE -C ./ VERSION -C libs/ licenses" >> Makefile
 
 cat Makefile.base >> Makefile
 
index 3d3b23bb00573c2ffb12d00c61915dcfd645fcad..5ab6912a8707fcf8121728b3ff378d676273fc35 100755 (executable)
 
 ENABLED=true
 USER=fanfix
-
 JAR=/path/to/fanfix.jar
-PINCODE="my password"
-PORT=12000
 
 FPID=/tmp/fanfix.pid
 OUT=/var/log/fanfix
@@ -39,7 +36,7 @@ start)
        else
                [ -e "$OUT" ] && mv "$OUT" "$OUT".previous
                [ -e "$ERR" ] && mv "$ERR" "$ERR".previous
-               sudo -u "$USER" -- java -jar "$JAR" --server "$PINCODE" "$PORT" > "$OUT" 2> "$ERR" &
+               sudo -u "$USER" -- java -jar "$JAR" --server > "$OUT" 2> "$ERR" &
                echo $! > "$FPID"
        fi
        
@@ -48,7 +45,7 @@ start)
 ;;
 stop)
        if sh "$0" status --quiet; then
-               sudo -u "$USER" -- java -jar "$JAR" --stop-server "$PINCODE" "$PORT"
+               sudo -u "$USER" -- java -jar "$JAR" --stop-server
        fi
        
        i=1
diff --git a/libs/nikiroo-utils-4.5.2-sources.jar b/libs/nikiroo-utils-4.5.2-sources.jar
deleted file mode 100644 (file)
index 3a652cf..0000000
Binary files a/libs/nikiroo-utils-4.5.2-sources.jar and /dev/null differ
diff --git a/libs/nikiroo-utils-4.7.2-dev-sources.jar b/libs/nikiroo-utils-4.7.2-dev-sources.jar
new file mode 100644 (file)
index 0000000..74f4387
Binary files /dev/null and b/libs/nikiroo-utils-4.7.2-dev-sources.jar differ
diff --git a/src/AndroidManifest.xml b/src/AndroidManifest.xml
deleted file mode 100644 (file)
index 7e919ba..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="be.nikiroo.fanfix.reader.android">
-
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.INTERNET" />
-
-    <application
-        android:allowBackup="true"
-        android:icon="@mipmap/ic_launcher"
-        android:label="@string/app_name"
-        android:roundIcon="@mipmap/ic_launcher_round"
-        android:supportsRtl="true"
-        android:theme="@style/AppTheme">
-        <activity android:name="be.nikiroo.fanfix.reader.android.AndroidReaderActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
\ No newline at end of file
index e2af0709bb316e06ac78634d6b3aa877082ccc33..abc958501a9fcf04e80be952de2c8ec92e59b6b9 100644 (file)
@@ -11,7 +11,6 @@ import be.nikiroo.fanfix.supported.BasicSupport;
 import be.nikiroo.utils.Cache;
 import be.nikiroo.utils.CacheMemory;
 import be.nikiroo.utils.Downloader;
-import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
 import be.nikiroo.utils.ImageUtils;
 import be.nikiroo.utils.TraceHandler;
@@ -27,7 +26,7 @@ import be.nikiroo.utils.TraceHandler;
  */
 public class DataLoader {
        private Downloader downloader;
-       private Cache downloadCache;
+       private Downloader downloaderNoCache;
        private Cache cache;
 
        /**
@@ -51,9 +50,11 @@ public class DataLoader {
         */
        public DataLoader(File dir, String UA, int hoursChanging, int hoursStable)
                        throws IOException {
-               downloader = new Downloader(UA);
-               downloadCache = new Cache(dir, hoursChanging, hoursStable);
-               cache = downloadCache;
+               downloader = new Downloader(UA, new Cache(dir, hoursChanging,
+                               hoursStable));
+               downloaderNoCache = new Downloader(UA);
+
+               cache = downloader.getCache();
        }
 
        /**
@@ -65,7 +66,7 @@ public class DataLoader {
         */
        public DataLoader(String UA) {
                downloader = new Downloader(UA);
-               downloadCache = null;
+               downloaderNoCache = downloader;
                cache = new CacheMemory();
        }
 
@@ -77,9 +78,10 @@ public class DataLoader {
         */
        public void setTraceHandler(TraceHandler tracer) {
                downloader.setTraceHandler(tracer);
+               downloaderNoCache.setTraceHandler(tracer);
                cache.setTraceHandler(tracer);
-               if (downloadCache != null) {
-                       downloadCache.setTraceHandler(tracer);
+               if (downloader.getCache() != null) {
+                       downloader.getCache().setTraceHandler(tracer);
                }
 
        }
@@ -87,6 +89,8 @@ public class DataLoader {
        /**
         * Open a resource (will load it from the cache if possible, or save it into
         * the cache after downloading if not).
+        * <p>
+        * The cached resource will be assimilated to the given original {@link URL}
         * 
         * @param url
         *            the resource to open
@@ -102,8 +106,7 @@ public class DataLoader {
         */
        public InputStream open(URL url, BasicSupport support, boolean stable)
                        throws IOException {
-               // MUST NOT return null
-               return open(url, support, stable, url);
+               return open(url, url, support, stable, null, null, null);
        }
 
        /**
@@ -114,72 +117,71 @@ public class DataLoader {
         * 
         * @param url
         *            the resource to open
+        * @param originalUrl
+        *            the original {@link URL} before any redirection occurs, which
+        *            is also used for the cache ID if needed (so we can retrieve
+        *            the content with this URL if needed)
         * @param support
         *            the support to use to download the resource
         * @param stable
         *            TRUE for more stable resources, FALSE when they often change
-        * @param originalUrl
-        *            the original {@link URL} used to locate the cached resource
         * 
         * @return the opened resource, NOT NULL
         * 
         * @throws IOException
         *             in case of I/O error
         */
-       public InputStream open(URL url, BasicSupport support, boolean stable,
-                       URL originalUrl) throws IOException {
-               // MUST NOT return null
-               try {
-                       InputStream in = null;
-
-                       if (downloadCache != null) {
-                               in = downloadCache.load(originalUrl, false, stable);
-                               Instance.getTraceHandler().trace(
-                                               "Cache " + (in != null ? "hit" : "miss") + ": " + url);
-                       }
-
-                       if (in == null) {
-                               try {
-                                       in = openNoCache(url, support, null, null, null);
-                                       if (downloadCache != null) {
-                                               downloadCache.save(in, originalUrl);
-                                               // ..But we want a resetable stream
-                                               in.close();
-                                               in = downloadCache.load(originalUrl, true, stable);
-                                       } else {
-                                               InputStream resetIn = IOUtils.forceResetableStream(in);
-                                               if (resetIn != in) {
-                                                       in.close();
-                                                       in = resetIn;
-                                               }
-                                       }
-                               } catch (IOException e) {
-                                       throw new IOException("Cannot save the url: "
-                                                       + (url == null ? "null" : url.toString()), e);
-                               }
-                       }
-
-                       return in;
-               } catch (IOException e) {
-                       throw new IOException("Cannot open the url: "
-                                       + (url == null ? "null" : url.toString()), e);
-               }
+       public InputStream open(URL url, URL originalUrl, BasicSupport support,
+                       boolean stable) throws IOException {
+               return open(url, originalUrl, support, stable, null, null, null);
        }
 
        /**
-        * Open the given {@link URL} without using the cache, but still update the
-        * cookies.
+        * Open a resource (will load it from the cache if possible, or save it into
+        * the cache after downloading if not).
+        * <p>
+        * The cached resource will be assimilated to the given original {@link URL}
         * 
         * @param url
-        *            the {@link URL} to open
+        *            the resource to open
+        * @param originalUrl
+        *            the original {@link URL} before any redirection occurs, which
+        *            is also used for the cache ID if needed (so we can retrieve
+        *            the content with this URL if needed)
+        * @param support
+        *            the support to use to download the resource
+        * @param stable
+        *            TRUE for more stable resources, FALSE when they often change
+        * @param postParams
+        *            the POST parameters
+        * @param getParams
+        *            the GET parameters (priority over POST)
+        * @param oauth
+        *            OAuth authorization (aka, "bearer XXXXXXX")
         * 
-        * @return the {@link InputStream} of the opened page
+        * @return the opened resource, NOT NULL
         * 
         * @throws IOException
         *             in case of I/O error
         */
-       public InputStream openNoCache(URL url) throws IOException {
-               return downloader.open(url);
+       public InputStream open(URL url, URL originalUrl, BasicSupport support,
+                       boolean stable, Map<String, String> postParams,
+                       Map<String, String> getParams, String oauth) throws IOException {
+
+               Map<String, String> cookiesValues = null;
+               URL currentReferer = url;
+
+               if (support != null) {
+                       cookiesValues = support.getCookies();
+                       currentReferer = support.getCurrentReferer();
+                       // priority: arguments
+                       if (oauth == null) {
+                               oauth = support.getOAuth();
+                       }
+               }
+
+               return downloader.open(url, originalUrl, currentReferer, cookiesValues,
+                               postParams, getParams, oauth, stable);
        }
 
        /**
@@ -217,8 +219,8 @@ public class DataLoader {
                        }
                }
 
-               return downloader.open(url, currentReferer, cookiesValues, postParams,
-                               getParams, oauth);
+               return downloaderNoCache.open(url, currentReferer, cookiesValues,
+                               postParams, getParams, oauth);
        }
 
        /**
@@ -236,8 +238,8 @@ public class DataLoader {
         */
        public void refresh(URL url, BasicSupport support, boolean stable)
                        throws IOException {
-               if (downloadCache != null && !downloadCache.check(url, false, stable)) {
-                       open(url, support, stable).close();
+               if (!check(url, stable)) {
+                       open(url, url, support, stable, null, null, null).close();
                }
        }
 
@@ -254,7 +256,8 @@ public class DataLoader {
         * 
         */
        public boolean check(URL url, boolean stable) {
-               return downloadCache != null && downloadCache.check(url, false, stable);
+               return downloader.getCache() != null
+                               && downloader.getCache().check(url, false, stable);
        }
 
        /**
index cacbbfe33320c32df5002563105e643a9ac8c86a..96d5d6f9a3031124cce38f8d59f81ea53138df2e 100644 (file)
@@ -17,6 +17,8 @@ import be.nikiroo.fanfix.library.LocalLibrary;
 import be.nikiroo.fanfix.library.RemoteLibrary;
 import be.nikiroo.utils.Cache;
 import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.Proxy;
 import be.nikiroo.utils.TempFiles;
 import be.nikiroo.utils.TraceHandler;
 import be.nikiroo.utils.resources.Bundles;
@@ -40,9 +42,46 @@ public class Instance {
        private static TraceHandler tracer;
        private static TempFiles tempFiles;
 
-       static {
+       private static boolean init;
+
+       /**
+        * Initialise the instance -- if already initialised, nothing will happen.
+        * <p>
+        * Before calling this method, you may call
+        * {@link Bundles#setDirectory(String)} if wanted.
+        */
+       static public void init() {
+               init(false);
+       }
+
+       /**
+        * Initialise the instance -- if already initialised, nothing will happen
+        * unless you pass TRUE to <tt>force</tt>.
+        * <p>
+        * Before calling this method, you may call
+        * {@link Bundles#setDirectory(String)} if wanted.
+        * <p>
+        * Note: forcing the initialisation can be dangerous, so make sure to only
+        * make it under controlled circumstances -- for instance, at the start of
+        * the program, you could call {@link Instance#init()}, change some settings
+        * because you want to force those settings (it will also forbid users to
+        * change them!) and then call {@link Instance#init(boolean)} with
+        * <tt>force</tt> set to TRUE.
+        * 
+        * @param force
+        *            force the initialisation even if already initialised
+        */
+       static public void init(boolean force) {
+               if (init && !force) {
+                       return;
+               }
+
+               init = true;
+
                // Before we can configure it:
-               tracer = new TraceHandler(true, checkEnv("DEBUG"), checkEnv("DEBUG"));
+               Boolean debug = checkEnv("DEBUG");
+               boolean trace = debug != null && debug;
+               tracer = new TraceHandler(true, trace, trace);
 
                // config dir:
                configDir = getConfigDir();
@@ -53,15 +92,13 @@ public class Instance {
                // Most of the rest is dependent upon this:
                createConfigs(configDir, false);
 
+               // Proxy support
+               Proxy.use(Instance.getConfig().getString(Config.USE_PROXY));
+
                // update tracer:
-               boolean debug = Instance.getConfig()
-                               .getBoolean(Config.DEBUG_ERR, false);
-               boolean trace = Instance.getConfig().getBoolean(Config.DEBUG_TRACE,
-                               false);
-
-               if (checkEnv("DEBUG")) {
-                       debug = true;
-                       trace = true;
+               if (debug == null) {
+                       debug = Instance.getConfig().getBoolean(Config.DEBUG_ERR, false);
+                       trace = Instance.getConfig().getBoolean(Config.DEBUG_TRACE, false);
                }
 
                tracer = new TraceHandler(true, debug, trace);
@@ -70,7 +107,8 @@ public class Instance {
                remoteDir = new File(configDir, "remote");
                lib = createDefaultLibrary(remoteDir);
 
-               // create cache
+               // create cache and TMP
+               Image.setTemporaryFilesRoot(new File(configDir, "tmp.images"));
                File tmp = getFile(Config.CACHE_DIR);
                if (tmp == null) {
                        // Could have used: System.getProperty("java.io.tmpdir")
@@ -404,7 +442,8 @@ public class Instance {
                        trans.deleteFile(configDir);
                }
 
-               if (checkEnv("NOUTF")) {
+               Boolean noutf = checkEnv("NOUTF");
+               if (noutf != null && noutf) {
                        trans.setUnicode(false);
                        transGui.setUnicode(false);
                }
@@ -437,6 +476,7 @@ public class Instance {
                                                                + getFile(libDir), e));
                        }
                } else {
+                       Exception ex = null;
                        int pos = remoteLib.lastIndexOf(":");
                        if (pos >= 0) {
                                String port = remoteLib.substring(pos + 1).trim();
@@ -455,13 +495,14 @@ public class Instance {
                                                                lib);
 
                                        } catch (Exception e) {
+                                               ex = e;
                                        }
                                }
                        }
 
                        if (lib == null) {
                                tracer.error(new IOException(
-                                               "Cannot create remote library for: " + remoteLib));
+                                               "Cannot create remote library for: " + remoteLib, ex));
                        }
                }
 
@@ -545,7 +586,7 @@ public class Instance {
        private static String getLang() {
                String lang = config.getString(Config.LANG);
 
-               if (lang == null | lang.isEmpty()) {
+               if (lang == null || lang.isEmpty()) {
                        if (System.getenv("LANG") != null
                                        && !System.getenv("LANG").isEmpty()) {
                                lang = System.getenv("LANG");
@@ -567,7 +608,7 @@ public class Instance {
         * 
         * @return TRUE if it is
         */
-       private static boolean checkEnv(String key) {
+       private static Boolean checkEnv(String key) {
                String value = System.getenv(key);
                if (value != null) {
                        value = value.trim().toLowerCase();
@@ -576,8 +617,10 @@ public class Instance {
                                        || "y".equals(value)) {
                                return true;
                        }
+
+                       return false;
                }
 
-               return false;
+               return null;
        }
 }
index 953bc45a50b776a99b533fab67bbd9dc5dafa7d9..b3633612713996f8651bffda5f480fc2607b54be 100644 (file)
@@ -4,11 +4,14 @@ import java.io.File;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.List;
 
+import javax.net.ssl.SSLException;
+
+import be.nikiroo.fanfix.bundles.Config;
 import be.nikiroo.fanfix.bundles.StringId;
 import be.nikiroo.fanfix.data.Chapter;
-import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.BasicLibrary;
 import be.nikiroo.fanfix.library.CacheLibrary;
@@ -20,6 +23,7 @@ import be.nikiroo.fanfix.output.BasicOutput.OutputType;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.reader.Reader;
 import be.nikiroo.fanfix.reader.Reader.ReaderType;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
 import be.nikiroo.fanfix.supported.BasicSupport;
 import be.nikiroo.fanfix.supported.SupportType;
 import be.nikiroo.utils.Progress;
@@ -33,7 +37,7 @@ import be.nikiroo.utils.serial.server.ServerObject;
  */
 public class Main {
        private enum MainAction {
-               IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION, SERVER, STOP_SERVER, REMOTE, SET_SOURCE, SET_TITLE, SET_AUTHOR
+               IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION, SERVER, STOP_SERVER, REMOTE, SET_SOURCE, SET_TITLE, SET_AUTHOR, SEARCH, SEARCH_TAG
        }
 
        /**
@@ -59,6 +63,14 @@ public class Main {
         * </li>
         * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
         * story, without saving it</li>
+        * <li>--search: list the supported websites (where)</li>
+        * <li>--search [where] [keywords] (page [page]) (item [item]): search on
+        * the supported website and display the given results page of stories it
+        * found, or the story details if asked</li>
+        * <li>--search-tag [where]: list all the tags supported by this website</li>
+        * <li>--search-tag [index 1]... (page [page]) (item [item]): search for the
+        * given stories or subtags, tag by tag, and display information about a
+        * specific page of results or about a specific item if requested</li>
         * <li>--list ([type]): list the stories present in the library</li>
         * <li>--set-source [id] [new source]: change the source of the given story</li>
         * <li>--set-title [id] [new title]: change the title of the given story</li>
@@ -66,9 +78,8 @@ public class Main {
         * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
         * for this command</li>
         * <li>--version: get the version of the program</li>
-        * <li>--server [key] [port]: start a server on this port</li>
-        * <li>--stop-server [key] [port]: stop the running server on this port if
-        * any</li>
+        * <li>--server: start the server mode (see config file for parameters)</li>
+        * <li>--stop-server: stop the running server on this port if any</li>
         * <li>--remote [key] [host] [port]: use a the given remote library</li>
         * </ul>
         * 
@@ -76,6 +87,9 @@ public class Main {
         *            see method description
         */
        public static void main(String[] args) {
+               // Only one line, but very important:
+               Instance.init();
+
                String urlString = null;
                String luid = null;
                String sourceString = null;
@@ -88,6 +102,11 @@ public class Main {
                Boolean plusInfo = null;
                String host = null;
                Integer port = null;
+               SupportType searchOn = null;
+               String search = null;
+               List<Integer> tags = new ArrayList<Integer>();
+               Integer page = null;
+               Integer item = null;
 
                boolean noMoreActions = false;
 
@@ -200,6 +219,103 @@ public class Main {
                                        exitCode = 255;
                                }
                                break;
+                       case SEARCH:
+                               if (searchOn == null) {
+                                       searchOn = SupportType.valueOfAllOkUC(args[i]);
+
+                                       if (searchOn == null) {
+                                               Instance.getTraceHandler().error(
+                                                               "Website not known: <" + args[i] + ">");
+                                               exitCode = 41;
+                                               break;
+                                       }
+
+                                       if (BasicSearchable.getSearchable(searchOn) == null) {
+                                               Instance.getTraceHandler().error(
+                                                               "Website not supported: " + searchOn);
+                                               exitCode = 42;
+                                               break;
+                                       }
+                               } else if (search == null) {
+                                       search = args[i];
+                               } else if (page != null && page == -1) {
+                                       try {
+                                               page = Integer.parseInt(args[i]);
+                                       } catch (Exception e) {
+                                               page = -2;
+                                       }
+                               } else if (item != null && item == -1) {
+                                       try {
+                                               item = Integer.parseInt(args[i]);
+                                       } catch (Exception e) {
+                                               item = -2;
+                                       }
+                               } else if (page == null || item == null) {
+                                       if (page == null && "page".equals(args[i])) {
+                                               page = -1;
+                                       } else if (item == null && "item".equals(args[i])) {
+                                               item = -1;
+                                       } else {
+                                               exitCode = 255;
+                                       }
+                               } else {
+                                       exitCode = 255;
+                               }
+                               break;
+                       case SEARCH_TAG:
+                               if (searchOn == null) {
+                                       searchOn = SupportType.valueOfAllOkUC(args[i]);
+
+                                       if (searchOn == null) {
+                                               Instance.getTraceHandler().error(
+                                                               "Website not known: <" + args[i] + ">");
+                                               exitCode = 255;
+                                       }
+
+                                       if (BasicSearchable.getSearchable(searchOn) == null) {
+                                               Instance.getTraceHandler().error(
+                                                               "Website not supported: " + searchOn);
+                                               exitCode = 255;
+                                       }
+                               } else if (page == null && item == null) {
+                                       if ("page".equals(args[i])) {
+                                               page = -1;
+                                       } else if ("item".equals(args[i])) {
+                                               item = -1;
+                                       } else {
+                                               try {
+                                                       int index = Integer.parseInt(args[i]);
+                                                       tags.add(index);
+                                               } catch (NumberFormatException e) {
+                                                       Instance.getTraceHandler().error(
+                                                                       "Invalid tag index: " + args[i]);
+                                                       exitCode = 255;
+                                               }
+                                       }
+                               } else if (page != null && page == -1) {
+                                       try {
+                                               page = Integer.parseInt(args[i]);
+                                       } catch (Exception e) {
+                                               page = -2;
+                                       }
+                               } else if (item != null && item == -1) {
+                                       try {
+                                               item = Integer.parseInt(args[i]);
+                                       } catch (Exception e) {
+                                               item = -2;
+                                       }
+                               } else if (page == null || item == null) {
+                                       if (page == null && "page".equals(args[i])) {
+                                               page = -1;
+                                       } else if (item == null && "item".equals(args[i])) {
+                                               item = -1;
+                                       } else {
+                                               exitCode = 255;
+                                       }
+                               } else {
+                                       exitCode = 255;
+                               }
+                               break;
                        case HELP:
                                exitCode = 255;
                                break;
@@ -214,14 +330,10 @@ public class Main {
                                exitCode = 255; // no arguments for this option
                                break;
                        case SERVER:
+                               exitCode = 255; // no arguments for this option
+                               break;
                        case STOP_SERVER:
-                               if (key == null) {
-                                       key = args[i];
-                               } else if (port == null) {
-                                       port = Integer.parseInt(args[i]);
-                               } else {
-                                       exitCode = 255;
-                               }
+                               exitCode = 255; // no arguments for this option
                                break;
                        case REMOTE:
                                if (key == null) {
@@ -282,14 +394,14 @@ public class Main {
                                System.err.println("\tVersion " + v);
                                System.err.println("\t-------------");
                                System.err.println("");
-                               for (String item : updates.getChanges().get(v)) {
-                                       System.err.println("\t- " + item);
+                               for (String it : updates.getChanges().get(v)) {
+                                       System.err.println("\t- " + it);
                                }
                                System.err.println("");
                        }
                }
 
-               if (exitCode != 255) {
+               if (exitCode == 0) {
                        switch (action) {
                        case IMPORT:
                                exitCode = imprt(urlString, pg);
@@ -357,6 +469,81 @@ public class Main {
                                        break;
                                }
                                exitCode = read(urlString, chapString, false);
+                               break;
+                       case SEARCH:
+                               page = page == null ? 1 : page;
+                               if (page < 0) {
+                                       Instance.getTraceHandler().error("Incorrect page number");
+                                       exitCode = 255;
+                                       break;
+                               }
+
+                               item = item == null ? 0 : item;
+                               if (item < 0) {
+                                       Instance.getTraceHandler().error("Incorrect item number");
+                                       exitCode = 255;
+                                       break;
+                               }
+
+                               if (BasicReader.getReader() == null) {
+                                       Instance.getTraceHandler()
+                                                       .error(new Exception(
+                                                                       "No reader type has been configured"));
+                                       exitCode = 10;
+                                       break;
+                               }
+
+                               try {
+                                       if (searchOn == null) {
+                                               BasicReader.getReader().search(true);
+                                       } else if (search != null) {
+
+                                               BasicReader.getReader().search(searchOn, search, page,
+                                                               item, true);
+                                       } else {
+                                               exitCode = 255;
+                                       }
+                               } catch (IOException e1) {
+                                       Instance.getTraceHandler().error(e1);
+                                       exitCode = 20;
+                               }
+
+                               break;
+                       case SEARCH_TAG:
+                               if (searchOn == null) {
+                                       exitCode = 255;
+                                       break;
+                               }
+
+                               page = page == null ? 1 : page;
+                               if (page < 0) {
+                                       Instance.getTraceHandler().error("Incorrect page number");
+                                       exitCode = 255;
+                                       break;
+                               }
+
+                               item = item == null ? 0 : item;
+                               if (item < 0) {
+                                       Instance.getTraceHandler().error("Incorrect item number");
+                                       exitCode = 255;
+                                       break;
+                               }
+
+                               if (BasicReader.getReader() == null) {
+                                       Instance.getTraceHandler()
+                                                       .error(new Exception(
+                                                                       "No reader type has been configured"));
+                                       exitCode = 10;
+                                       break;
+                               }
+
+                               try {
+                                       BasicReader.getReader().searchTag(searchOn, page, item,
+                                                       true, tags.toArray(new Integer[] {}));
+                               } catch (IOException e1) {
+                                       Instance.getTraceHandler().error(e1);
+                               }
+
                                break;
                        case HELP:
                                syntax(true);
@@ -381,11 +568,19 @@ public class Main {
                                        exitCode = 10;
                                        break;
                                }
-                               BasicReader.getReader().browse(null);
+                               try {
+                                       BasicReader.getReader().browse(null);
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
+                                       exitCode = 66;
+                               }
                                break;
                        case SERVER:
+                               key = Instance.getConfig().getString(Config.SERVER_KEY);
+                               port = Instance.getConfig().getInteger(Config.SERVER_PORT);
                                if (port == null) {
-                                       exitCode = 255;
+                                       System.err.println("No port configured in the config file");
+                                       exitCode = 15;
                                        break;
                                }
                                try {
@@ -397,12 +592,24 @@ public class Main {
                                }
                                return;
                        case STOP_SERVER:
+                               key = Instance.getConfig().getString(Config.SERVER_KEY);
+                               port = Instance.getConfig().getInteger(Config.SERVER_PORT);
                                if (port == null) {
-                                       exitCode = 255;
+                                       System.err.println("No port configured in the config file");
+                                       exitCode = 15;
                                        break;
                                }
+                               try {
+                                       new RemoteLibrary(key, host, port).exit();
+                               } catch (SSLException e) {
+                                       Instance.getTraceHandler().error(
+                                                       "Bad access key for remote library");
+                                       exitCode = 43;
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
+                                       exitCode = 44;
+                               }
 
-                               new RemoteLibrary(key, host, port).exit();
                                break;
                        case REMOTE:
                                exitCode = 255; // should not be reachable (REMOTE -> START)
@@ -494,18 +701,14 @@ public class Main {
         * @return the exit return code (0 = success)
         */
        private static int list(String source) {
-               List<MetaData> stories;
-               stories = BasicReader.getReader().getLibrary().getListBySource(source);
-
-               for (MetaData story : stories) {
-                       String author = "";
-                       if (story.getAuthor() != null && !story.getAuthor().isEmpty()) {
-                               author = " (" + story.getAuthor() + ")";
-                       }
-
-                       System.out.println(story.getLuid() + ": " + story.getTitle()
-                                       + author);
+               BasicReader.setDefaultReaderType(ReaderType.CLI);
+               try {
+                       BasicReader.getReader().browse(source);
+               } catch (IOException e) {
+                       Instance.getTraceHandler().error(e);
+                       return 66;
                }
+
                return 0;
        }
 
index 76758af85ca3daaa5ef4f5b8e5fcc799bbee6f2e..2c9a0328aca615aa982f58f68262b636e9758086 100644 (file)
@@ -120,7 +120,8 @@ public class VersionCheck {
                                InputStream in = null;
                                for (String url : new String[] { urlFrBE, urlFr, urlDefault }) {
                                        try {
-                                               in = Instance.getCache().openNoCache(new URL(url));
+                                               in = Instance.getCache()
+                                                               .open(new URL(url), null, false);
                                                break;
                                        } catch (IOException e) {
                                        }
index ecbfa3da9f0ce11ae8454106578c4f5f0f1c9b10..35f63d6dca7e06925ec08e799c348bf4778c7178 100644 (file)
@@ -32,6 +32,16 @@ public enum Config {
        DEFAULT_COVERS_DIR, //
        @Meta(description = "string", info = "The default library to use (KEY:SERVER:PORT), or empty for the local library")
        DEFAULT_LIBRARY, //
+       @Meta(def = "58365", description = "The port on which we can start the server", format = Format.INT, info = "A valid port")
+       SERVER_PORT, //
+       @Meta(def = "", description = "The encryption key for the server (NOT including a subkey)", format = Format.PASSWORD, info = "cannot contain the pipe character (|)")
+       SERVER_KEY, //
+       @Meta(def = "TRUE", description = "Allow write access to the clients by default (download story, move story...)", format = Format.BOOLEAN)
+       SERVER_RW, //
+       @Meta(def = "", description = "If not empty, only the EXACT listed sources will be available for clients", info = "list is comma-separated (,) and values are surrounded by double quotes (\"); any double quote in the value must be backslash-escaped (with \\\")")
+       SERVER_WHITELIST, //
+       @Meta(def = "", description = "The subkeys that the server will allow, including the modes", info = "list is comma-separated (,) and values are surrounded by double quotes (\"); any double quote in the value must be backslash-escaped (with \\\")")
+       SERVER_ALLOWED_SUBKEYS, //
        @Meta(def = "$HOME/Books", description = "absolute path, $HOME variable supported, / is always accepted as dir separator", format = Format.DIRECTORY, info = "The directory where to store the library")
        LIBRARY_DIR, //
        @Meta(def = "false", description = "boolean", format = Format.BOOLEAN, info = "Show debug information on errors")
@@ -64,7 +74,8 @@ public enum Config {
        LOGIN_YIFFSTAR_PASS, //
        @Meta(description = "If the last update check was done at least that many days, check for updates at startup (-1 for 'no checks' -- default is 1 day)", format = Format.INT)
        UPDATE_INTERVAL, //
-       @Meta(description = "An API key required to create a token from FimFiction", format = Format.STRING)
+       @Meta(def = "", description = "", info = "Format is ((user(:pass)@)proxy:port), with ':' being system proxy and an empty String being no proxy")
+       USE_PROXY, @Meta(description = "An API key required to create a token from FimFiction", format = Format.STRING)
        LOGIN_FIMFICTION_APIKEY_CLIENT_ID, //
        @Meta(description = "An API key required to create a token from FimFiction", format = Format.PASSWORD)
        LOGIN_FIMFICTION_APIKEY_CLIENT_SECRET, //
index 8d3022e708037b3753fe898dd701c8c535a256c0..6bb774c1a0bab0e194f1c31da8e6b101f310e6ac 100644 (file)
@@ -119,6 +119,8 @@ public enum StringIdGui {
        MENU_EDIT_SET_COVER_FOR_SOURCE, //
        @Meta(def = "Set as cover for author", format = Format.STRING, description = "the edit/Set as cover for author menu button")
        MENU_EDIT_SET_COVER_FOR_AUTHOR, //
+       @Meta(def = "Search", format = Format.STRING, description = "the search menu to open the earch stories on one of the searchable websites")
+       MENU_SEARCH,
        @Meta(def = "View", format = Format.STRING, description = "the view menu")
        MENU_VIEW, //
        @Meta(def = "Word count", format = Format.STRING, description = "the view/word_count menu button, to show the word/image/story count as secondary info")
@@ -148,7 +150,7 @@ public enum StringIdGui {
        @Meta(def = "An error occured when contacting the library", format = Format.STRING, description = "default description if the error is not known")
        ERROR_LIB_STATUS, //
        @Meta(def = "You are not allowed to access this library", format = Format.STRING, description = "library access not allowed")
-       ERROR_LIB_STATUS_UNAUTORIZED, //
+       ERROR_LIB_STATUS_UNAUTHORIZED, //
        @Meta(def = "Library not valid", format = Format.STRING, description = "the library is invalid (not correctly set up)")
        ERROR_LIB_STATUS_INVALID, //
        @Meta(def = "Library currently unavailable", format = Format.STRING, description = "the library is out of commission")
index 8f22f9fb63e639e9a7af5be81fe3c06256a0ce40..63b7642a769e5773d183ec93422367a59c0bbf8d 100644 (file)
@@ -33,6 +33,21 @@ USER_AGENT = Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.
 DEFAULT_COVERS_DIR = $HOME/.fanfix/covers/
 # string (FORMAT: STRING) The default library to use (KEY:SERVER:PORT), or empty for the local library
 DEFAULT_LIBRARY = 
+# The port on which we can start the server
+# (FORMAT: INT) A valid port
+SERVER_PORT = 58365
+# The encryption key for the server (NOT including a subkey)
+# (FORMAT: PASSWORD) cannot contain the pipe character (|)
+SERVER_KEY = 
+# Allow write access to the clients by default (download story, move story...)
+# (FORMAT: BOOLEAN) 
+SERVER_RW = 
+# If not empty, only the EXACT listed sources will be available for clients
+# (FORMAT: STRING) list is comma-separated (,) and values are surrounded by double quotes ("); any double quote in the value must be backslash-escaped (with \")
+SERVER_WHITELIST = 
+# The subkeys that the server will allow, including the modes
+# (FORMAT: STRING) list is comma-separated (,) and values are surrounded by double quotes ("); any double quote in the value must be backslash-escaped (with \")
+SERVER_ALLOWED_SUBKEYS = 
 # absolute path, $HOME variable supported, / is always accepted as dir separator
 # (FORMAT: DIRECTORY) The directory where to store the library
 LIBRARY_DIR = $HOME/Books
@@ -77,6 +92,8 @@ LOGIN_YIFFSTAR_PASS =
 # If the last update check was done at least that many days, check for updates at startup (-1 for 'no checks' -- default is 1 day)
 # (FORMAT: INT) 
 UPDATE_INTERVAL = 
+#  (FORMAT: STRING) Format is ((user(:pass)@)proxy:port), with ':' being system proxy and an empty String being no proxy
+USE_PROXY = 
 # An API key required to create a token from FimFiction
 # (FORMAT: STRING) 
 LOGIN_FIMFICTION_APIKEY_CLIENT_ID = 
index 62fd158ecddf0288ddc1b7e684b0aa420e4665ce..1ebc3d5932a5f86ef8cf87b3a1143058ee172939 100644 (file)
@@ -16,14 +16,26 @@ HELP_SYNTAX = Valid options:\n\
 \t--read [id] ([chapter number]): read the given story from the library\n\
 \t--read-url [URL] ([chapter number]): convert on the fly and read the \n\
 \t\tstory, without saving it\n\
+\t--search WEBSITE [free text] ([page] ([item])): search for the given terms, show the\n\
+\t\tgiven page (page 0 means "how many page do we have", starts at page 1)\n\
+\t--search-tag WEBSITE ([tag 1] [tag2...] ([page] ([item]))): list the known tags or \n\
+\t\tsearch the stories for the given tag(s), show the given page of results\n\
+\t--search: list the supported websites (where)\n\
+\t--search [where] [keywords] (page [page]) (item [item]): search on the supported \n\
+\t\twebsite and display the given results page of stories it found, or the story \n\
+\t\tdetails if asked\n\
+\t--search-tag [where]: list all the tags supported by this website\n\
+\t--search-tag [index 1]... (page [page]) (item [item]): search for the given stories or \n\
+\t\tsubtags, tag by tag, and display information about a specific page of results or \n\
+\t\tabout a specific item if requested\n\
 \t--list ([type]) : list the stories present in the library\n\
 \t--set-source [id] [new source]: change the source of the given story\n\
 \t--set-title [id] [new title]: change the title of the given story\n\
 \t--set-author [id] [new author]: change the author of the given story\n\
 \t--set-reader [reader type]: set the reader type to CLI, TUI or GUI for \n\
 \t\tthis command\n\
-\t--server [key] [port]: start a remote server on this port\n\
-\t--stop-server [key] [port]: stop the remote server running on this port\n\
+\t--server: start the server mode (see config file for parameters)\n\
+\t--stop-server: stop the remote server running on this port\n\
 \t\tif any (key must be set to the same value)\n\
 \t--remote [key] [host] [port]: select this remote server to get \n\
 \t\t(or update or...) the stories from (key must be set to the \n\
index 092bd336248e211b2c4d573b0136597355c5a026..e64651b3275d781953f4f8ba3ad9b66a993c1907 100644 (file)
@@ -15,13 +15,21 @@ HELP_SYNTAX = Options reconnues :\n\
 \t--convert [URL] [output_type] [target] (+info): convertir l'histoire vers le fichier donné, et forcer l'ajout d'un fichier .info si +info est utilisé\n\
 \t--read [id] ([chapter number]): afficher l'histoire "id"\n\
 \t--read-url [URL] ([chapter number]): convertir l'histoire et la lire à la volée, sans la sauver\n\
+\t--search: liste les sites supportés (where)\n\
+\t--search [where] [keywords] (page [page]) (item [item]): lance une recherche et \n\
+\t\taffiche les résultats de la page page (page 1 par défaut), et de l'item item \n\
+\t\tspécifique si demandé\n\
+\t--search-tag [where]: liste tous les tags supportés par ce site web\n\
+\t--search-tag [index 1]... (page [page]) (item [item]): affine la recherche, tag par tag,\n\
+\t\tet affiche si besoin les sous-tags, les histoires ou les infos précises de \n\
+\t\tl'histoire demandée\n\
 \t--list ([type]): lister les histoires presentes dans la librairie et leurs IDs\n\
 \t--set-source [id] [nouvelle source]: change la source de l'histoire\n\
 \t--set-title [id] [nouveau titre]: change le titre de l'histoire\n\
 \t--set-author [id] [nouvel auteur]: change l'auteur de l'histoire\n\
 \t--set-reader [reader type]: changer le type de lecteur pour la commande en cours sur CLI, TUI ou GUI\n\
-\t--server [key] [port]: démarrer un serveur d'histoires sur ce port\n\
-\t--stop-server [key] [port]: arrêter le serveur distant sur ce port (key doit avoir la même valeur) \n\
+\t--server: démarre le mode serveur (les paramètres sont dans le fichier de config)\n\
+\t--stop-server: arrêter le serveur distant sur ce port (key doit avoir la même valeur) \n\
 \t--remote [key] [host] [port]: contacter ce server au lieu de la librairie habituelle (key doit avoir la même valeur)\n\
 \t--help: afficher la liste des options disponibles\n\
 \t--version: retourne la version du programme\n\
index 5e49ceb43b3638a8fbb02dbf8f205f5369acd8f9..de44c18c085b11663cfae8b7b4546f9c2d71dc10 100644 (file)
@@ -125,6 +125,9 @@ MENU_EDIT_SET_COVER_FOR_SOURCE = Set as cover for source
 # the edit/Set as cover for author menu button
 # (FORMAT: STRING) 
 MENU_EDIT_SET_COVER_FOR_AUTHOR = Set as cover for author
+# the search menu to open the earch stories on one of the searchable websites
+# (FORMAT: STRING) 
+MENU_SEARCH = Search
 # the view menu (FORMAT: STRING) 
 MENU_VIEW = View
 # the view/word_count menu button, to show the word/image/story count as secondary info
@@ -162,7 +165,7 @@ PROGRESS_CHANGE_SOURCE = Change the source of the book to %s
 ERROR_LIB_STATUS = An error occured when contacting the library
 # library access not allowed
 # (FORMAT: STRING) 
-ERROR_LIB_STATUS_UNAUTORIZED = You are not allowed to access this library
+ERROR_LIB_STATUS_UNAUTHORIZED = You are not allowed to access this library
 # the library is invalid (not correctly set up)
 # (FORMAT: STRING) 
 ERROR_LIB_STATUS_INVALID = Library not valid
index 6e14c98ffd670567b8db41f2fab2586ffb60e282..2b6d19200b0773404d03ee49e052cc58fe6ee1b2 100644 (file)
@@ -125,6 +125,9 @@ MENU_EDIT_SET_COVER_FOR_SOURCE = Utiliser comme cover pour la source
 # the edit/Set as cover for author menu button
 # (FORMAT: STRING) 
 MENU_EDIT_SET_COVER_FOR_AUTHOR = Utiliser comme cover pour l'auteur
+# the search menu to open the earch stories on one of the searchable websites
+# (FORMAT: STRING) 
+MENU_SEARCH = Recherche
 # the view menu (FORMAT: STRING) 
 MENU_VIEW = Affichage
 # the view/word_count menu button, to show the word/image/story count as secondary info
@@ -162,7 +165,7 @@ PROGRESS_CHANGE_SOURCE = Change la source du livre en %s
 ERROR_LIB_STATUS = Une erreur est survenue en contactant la librairie
 # library access not allowed
 # (FORMAT: STRING) 
-ERROR_LIB_STATUS_UNAUTORIZED = Vous n'étes pas autorisé à accéder à cette librairie
+ERROR_LIB_STATUS_UNAUTHORIZED = Vous n'étes pas autorisé à accéder à cette librairie
 # the library is invalid (not correctly set up)
 # (FORMAT: STRING) 
 ERROR_LIB_STATUS_INVALID = Librairie invalide
index 873dcb801f89b03f5ca648bd9c2f67f84ab761f6..d490058a4703b51e042bb369effdf4b4fa511be6 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.fanfix.data;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -9,7 +10,9 @@ import java.util.List;
  * 
  * @author niki
  */
-public class Chapter implements Iterable<Paragraph>, Cloneable {
+public class Chapter implements Iterable<Paragraph>, Cloneable, Serializable {
+       private static final long serialVersionUID = 1L;
+       
        private String name;
        private int number;
        private List<Paragraph> paragraphs = new ArrayList<Paragraph>();
index cbaf84e3c75def8646332cb7975a650dfe703013..1781d869f263c559d6e7f5240399db5e498a4fd5 100644 (file)
@@ -1,16 +1,20 @@
 package be.nikiroo.fanfix.data;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
 import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
 
 /**
  * The meta data associated to a {@link Story} object.
  * 
  * @author niki
  */
-public class MetaData implements Cloneable, Comparable<MetaData> {
+public class MetaData implements Cloneable, Comparable<MetaData>, Serializable {
+       private static final long serialVersionUID = 1L;
+
        private String title;
        private String author;
        private String date;
@@ -466,19 +470,8 @@ public class MetaData implements Cloneable, Comparable<MetaData> {
 
                String cover = "none";
                if (getCover() != null) {
-                       cover = " bytes";
-
-                       int size = getCover().getData().length;
-                       if (size > 1000) {
-                               size /= 1000;
-                               cover = " kb";
-                               if (size > 1000) {
-                                       size /= 1000;
-                                       cover = " mb";
-                               }
-                       }
-
-                       cover = size + cover;
+                       cover = StringUtils.formatNumber(getCover().getData().length)
+                                       + "bytes";
                }
 
                return String.format(
index 0ed61fbb614f494725ebbc0f1c632a7ffcd7a29a..9adc51c420e815492858adbcffbd1b606dab7eda 100644 (file)
@@ -1,5 +1,7 @@
 package be.nikiroo.fanfix.data;
 
+import java.io.Serializable;
+
 import be.nikiroo.utils.Image;
 
 /**
@@ -7,7 +9,9 @@ import be.nikiroo.utils.Image;
  * 
  * @author niki
  */
-public class Paragraph implements Cloneable {
+public class Paragraph implements Cloneable, Serializable {
+       private static final long serialVersionUID = 1L;
+
        /**
         * A paragraph type, that will dictate how the paragraph will be handled.
         * 
index 0e0279f096adad91003c62e290fdefb60f1cfec6..fc3f909880031910c5b1ff698b247c949d6ffe38 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.fanfix.data;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -9,7 +10,9 @@ import java.util.List;
  * 
  * @author niki
  */
-public class Story implements Iterable<Chapter>, Cloneable {
+public class Story implements Iterable<Chapter>, Cloneable, Serializable {
+       private static final long serialVersionUID = 1L;
+       
        private MetaData meta;
        private List<Chapter> chapters = new ArrayList<Chapter>();
        private List<Chapter> empty = new ArrayList<Chapter>();
index aaa02c3d1e0df7881c3540113770c41d25f7bcca..57db36b42047aa08e7ccf1050077c793e3a1d6a3 100644 (file)
@@ -1,6 +1,8 @@
 /**
  * This package contains the data structure used by the program, without the 
  * logic behind them.
+ * <p>
+ * All the classes inside are serializable.
  * 
  * @author niki
  */
index 7f7a09dd528d63ce0322150de77c1c4bf683c4a8..380c5c988ca43dad853f7e4edb25e657a55ed1ba 100644 (file)
@@ -39,14 +39,36 @@ abstract public class BasicLibrary {
         * @author niki
         */
        public enum Status {
-               /** The library is ready. */
-               READY,
+               /** The library is ready and r/w. */
+               READ_WRITE,
+               /** The library is ready, but read-only. */
+               READ_ONLY,
                /** The library is invalid (not correctly set up). */
                INVALID,
                /** You are not allowed to access this library. */
-               UNAUTORIZED,
+               UNAUTHORIZED,
                /** The library is currently out of commission. */
-               UNAVAILABLE,
+               UNAVAILABLE;
+
+               /**
+                * The library is available (you can query it).
+                * <p>
+                * It does <b>not</b> specify if it is read-only or not.
+                * 
+                * @return TRUE if it is
+                */
+               public boolean isReady() {
+                       return (this == READ_WRITE || this == READ_ONLY);
+               }
+
+               /**
+                * This library can be modified (= you are allowed to modify it).
+                * 
+                * @return TRUE if it is
+                */
+               public boolean isWritable() {
+                       return (this == READ_WRITE);
+               }
        }
 
        /**
@@ -66,7 +88,7 @@ abstract public class BasicLibrary {
         * @return the current status
         */
        public Status getStatus() {
-               return Status.READY;
+               return Status.READ_WRITE;
        }
 
        /**
@@ -81,8 +103,11 @@ abstract public class BasicLibrary {
         *            the optional {@link Progress}
         * 
         * @return the corresponding {@link Story}
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public abstract File getFile(String luid, Progress pg);
+       public abstract File getFile(String luid, Progress pg) throws IOException;
 
        /**
         * Return the cover image associated to this story.
@@ -91,8 +116,11 @@ abstract public class BasicLibrary {
         *            the Library UID of the story
         * 
         * @return the cover image
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public abstract Image getCover(String luid);
+       public abstract Image getCover(String luid) throws IOException;
 
        /**
         * Return the cover image associated to this source.
@@ -104,8 +132,11 @@ abstract public class BasicLibrary {
         *            the source
         * 
         * @return the cover image or NULL
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public Image getSourceCover(String source) {
+       public Image getSourceCover(String source) throws IOException {
                Image custom = getCustomSourceCover(source);
                if (custom != null) {
                        return custom;
@@ -129,8 +160,11 @@ abstract public class BasicLibrary {
         *            the author
         * 
         * @return the cover image or NULL
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public Image getAuthorCover(String author) {
+       public Image getAuthorCover(String author) throws IOException {
                Image custom = getCustomAuthorCover(author);
                if (custom != null) {
                        return custom;
@@ -153,8 +187,12 @@ abstract public class BasicLibrary {
         *            the source to look for
         * 
         * @return the custom cover or NULL if none
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public Image getCustomSourceCover(@SuppressWarnings("unused") String source) {
+       @SuppressWarnings("unused")
+       public Image getCustomSourceCover(String source) throws IOException {
                return null;
        }
 
@@ -167,8 +205,12 @@ abstract public class BasicLibrary {
         *            the author to look for
         * 
         * @return the custom cover or NULL if none
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public Image getCustomAuthorCover(@SuppressWarnings("unused") String author) {
+       @SuppressWarnings("unused")
+       public Image getCustomAuthorCover(String author) throws IOException {
                return null;
        }
 
@@ -179,8 +221,12 @@ abstract public class BasicLibrary {
         *            the source to change
         * @param luid
         *            the story LUID
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public abstract void setSourceCover(String source, String luid);
+       public abstract void setSourceCover(String source, String luid)
+                       throws IOException;
 
        /**
         * Set the author cover to the given story cover.
@@ -189,8 +235,12 @@ abstract public class BasicLibrary {
         *            the author to change
         * @param luid
         *            the story LUID
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public abstract void setAuthorCover(String author, String luid);
+       public abstract void setAuthorCover(String author, String luid)
+                       throws IOException;
 
        /**
         * Return the list of stories (represented by their {@link MetaData}, which
@@ -200,8 +250,11 @@ abstract public class BasicLibrary {
         *            the optional {@link Progress}
         * 
         * @return the list (can be empty but not NULL)
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       protected abstract List<MetaData> getMetas(Progress pg);
+       protected abstract List<MetaData> getMetas(Progress pg) throws IOException;
 
        /**
         * Invalidate the {@link Story} cache (when the content should be re-read
@@ -228,8 +281,11 @@ abstract public class BasicLibrary {
         * 
         * @param meta
         *            the {@link Story} to clear from the cache
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       protected abstract void updateInfo(MetaData meta);
+       protected abstract void updateInfo(MetaData meta) throws IOException;
 
        /**
         * Return the next LUID that can be used.
@@ -274,15 +330,22 @@ abstract public class BasicLibrary {
         *            the optional progress reporter
         */
        public void refresh(Progress pg) {
-               getMetas(pg);
+               try {
+                       getMetas(pg);
+               } catch (IOException e) {
+                       // We will let it fail later
+               }
        }
 
        /**
         * List all the known types (sources) of stories.
         * 
         * @return the sources
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized List<String> getSources() {
+       public synchronized List<String> getSources() throws IOException {
                List<String> list = new ArrayList<String>();
                for (MetaData meta : getMetas(null)) {
                        String storySource = meta.getSource();
@@ -308,8 +371,12 @@ abstract public class BasicLibrary {
         * </ul>
         * 
         * @return the grouped list
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized Map<String, List<String>> getSourcesGrouped() {
+       public synchronized Map<String, List<String>> getSourcesGrouped()
+                       throws IOException {
                Map<String, List<String>> map = new TreeMap<String, List<String>>();
                for (String source : getSources()) {
                        String name;
@@ -340,8 +407,11 @@ abstract public class BasicLibrary {
         * List all the known authors of stories.
         * 
         * @return the authors
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized List<String> getAuthors() {
+       public synchronized List<String> getAuthors() throws IOException {
                List<String> list = new ArrayList<String>();
                for (MetaData meta : getMetas(null)) {
                        String storyAuthor = meta.getAuthor();
@@ -372,8 +442,11 @@ abstract public class BasicLibrary {
         * <tt>0-9</tt>, which may only be present or not).
         * 
         * @return the authors' names, grouped by letter(s)
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public Map<String, List<String>> getAuthorsGrouped() {
+       public Map<String, List<String>> getAuthorsGrouped() throws IOException {
                int MAX = 20;
 
                Map<String, List<String>> groups = new TreeMap<String, List<String>>();
@@ -448,7 +521,8 @@ abstract public class BasicLibrary {
         * @param car
         *            the starting character, <tt>*</tt>, <tt>0</tt> or a capital
         *            letter
-        * @return the authors that fulfill the starting letter
+        * 
+        * @return the authors that fulfil the starting letter
         */
        private List<String> getAuthorsGroup(List<String> authors, char car) {
                List<String> accepted = new ArrayList<String>();
@@ -480,8 +554,11 @@ abstract public class BasicLibrary {
         * Cover images <b>MAYBE</b> not included.
         * 
         * @return the stories
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized List<MetaData> getList() {
+       public synchronized List<MetaData> getList() throws IOException {
                return getMetas(null);
        }
 
@@ -495,8 +572,12 @@ abstract public class BasicLibrary {
         *            the type of story to retrieve, or NULL for all
         * 
         * @return the stories
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized List<MetaData> getListBySource(String type) {
+       public synchronized List<MetaData> getListBySource(String type)
+                       throws IOException {
                List<MetaData> list = new ArrayList<MetaData>();
                for (MetaData meta : getMetas(null)) {
                        String storyType = meta.getSource();
@@ -519,8 +600,12 @@ abstract public class BasicLibrary {
         *            the author of the stories to retrieve, or NULL for all
         * 
         * @return the stories
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized List<MetaData> getListByAuthor(String author) {
+       public synchronized List<MetaData> getListByAuthor(String author)
+                       throws IOException {
                List<MetaData> list = new ArrayList<MetaData>();
                for (MetaData meta : getMetas(null)) {
                        String storyAuthor = meta.getAuthor();
@@ -541,8 +626,11 @@ abstract public class BasicLibrary {
         *            the Library UID of the story
         * 
         * @return the corresponding {@link Story}
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized MetaData getInfo(String luid) {
+       public synchronized MetaData getInfo(String luid) throws IOException {
                if (luid != null) {
                        for (MetaData meta : getMetas(null)) {
                                if (luid.equals(meta.getLuid())) {
@@ -563,8 +651,12 @@ abstract public class BasicLibrary {
         *            the optional progress reporter
         * 
         * @return the corresponding {@link Story} or NULL if not found
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
-       public synchronized Story getStory(String luid, Progress pg) {
+       public synchronized Story getStory(String luid, Progress pg)
+                       throws IOException {
                Progress pgMetas = new Progress();
                Progress pgStory = new Progress();
                if (pg != null) {
@@ -598,9 +690,13 @@ abstract public class BasicLibrary {
         *            the optional progress reporter
         * 
         * @return the corresponding {@link Story} or NULL if not found
+        * 
+        * @throws IOException
+        *             in case of IOException
         */
        public synchronized Story getStory(String luid,
-                       @SuppressWarnings("javadoc") MetaData meta, Progress pg) {
+                       @SuppressWarnings("javadoc") MetaData meta, Progress pg)
+                       throws IOException {
 
                if (pg == null) {
                        pg = new Progress();
index 8f6e9c29a24bfd128c64e5fe69e90c98a53ac4e3..019acd210d1fb3b2263db468e2c21e6e6bfd1d69 100644 (file)
@@ -52,7 +52,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       protected List<MetaData> getMetas(Progress pg) {
+       protected List<MetaData> getMetas(Progress pg) throws IOException {
                if (pg == null) {
                        pg = new Progress();
                }
@@ -66,7 +66,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public synchronized MetaData getInfo(String luid) {
+       public synchronized MetaData getInfo(String luid) throws IOException {
                MetaData info = cacheLib.getInfo(luid);
                if (info == null) {
                        info = lib.getInfo(luid);
@@ -76,7 +76,8 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public synchronized Story getStory(String luid, MetaData meta, Progress pg) {
+       public synchronized Story getStory(String luid, MetaData meta, Progress pg)
+                       throws IOException {
                if (pg == null) {
                        pg = new Progress();
                }
@@ -109,7 +110,8 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public synchronized File getFile(final String luid, Progress pg) {
+       public synchronized File getFile(final String luid, Progress pg)
+                       throws IOException {
                if (pg == null) {
                        pg = new Progress();
                }
@@ -134,7 +136,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public Image getCover(final String luid) {
+       public Image getCover(final String luid) throws IOException {
                if (isCached(luid)) {
                        return cacheLib.getCover(luid);
                }
@@ -144,7 +146,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public Image getSourceCover(String source) {
+       public Image getSourceCover(String source) throws IOException {
                Image custom = getCustomSourceCover(source);
                if (custom != null) {
                        return custom;
@@ -159,7 +161,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public Image getAuthorCover(String author) {
+       public Image getAuthorCover(String author) throws IOException {
                Image custom = getCustomAuthorCover(author);
                if (custom != null) {
                        return custom;
@@ -174,7 +176,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public Image getCustomSourceCover(String source) {
+       public Image getCustomSourceCover(String source) throws IOException {
                Image custom = cacheLib.getCustomSourceCover(source);
                if (custom == null) {
                        custom = lib.getCustomSourceCover(source);
@@ -187,7 +189,7 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public Image getCustomAuthorCover(String author) {
+       public Image getCustomAuthorCover(String author) throws IOException {
                Image custom = cacheLib.getCustomAuthorCover(author);
                if (custom == null) {
                        custom = lib.getCustomAuthorCover(author);
@@ -200,19 +202,19 @@ public class CacheLibrary extends BasicLibrary {
        }
 
        @Override
-       public void setSourceCover(String source, String luid) {
+       public void setSourceCover(String source, String luid) throws IOException {
                lib.setSourceCover(source, luid);
                cacheLib.setSourceCover(source, getCover(luid));
        }
 
        @Override
-       public void setAuthorCover(String author, String luid) {
+       public void setAuthorCover(String author, String luid) throws IOException {
                lib.setAuthorCover(author, luid);
                cacheLib.setAuthorCover(author, getCover(luid));
        }
 
        @Override
-       protected void updateInfo(MetaData meta) {
+       protected void updateInfo(MetaData meta) throws IOException {
                if (meta != null && metas != null) {
                        for (int i = 0; i < metas.size(); i++) {
                                if (metas.get(i).getLuid().equals(meta.getLuid())) {
@@ -317,7 +319,11 @@ public class CacheLibrary extends BasicLibrary {
         * @return TRUE if it is
         */
        public boolean isCached(String luid) {
-               return cacheLib.getInfo(luid) != null;
+               try {
+                       return cacheLib.getInfo(luid) != null;
+               } catch (IOException e) {
+                       return false;
+               }
        }
 
        /**
index 59310fdc4f906b3247efb2b796e887c94666c41b..3b0a8489aae281cd929e4f3de06b5376f73ba677 100644 (file)
@@ -100,7 +100,7 @@ public class LocalLibrary extends BasicLibrary {
        }
 
        @Override
-       public File getFile(String luid, Progress pg) {
+       public File getFile(String luid, Progress pg) throws IOException {
                Instance.getTraceHandler().trace(
                                this.getClass().getSimpleName() + ": get file for " + luid);
 
@@ -122,7 +122,7 @@ public class LocalLibrary extends BasicLibrary {
        }
 
        @Override
-       public Image getCover(String luid) {
+       public Image getCover(String luid) throws IOException {
                MetaData meta = getInfo(luid);
                if (meta != null) {
                        if (meta.getCover() != null) {
@@ -288,12 +288,12 @@ public class LocalLibrary extends BasicLibrary {
        }
 
        @Override
-       public void setSourceCover(String source, String luid) {
+       public void setSourceCover(String source, String luid) throws IOException {
                setSourceCover(source, getCover(luid));
        }
 
        @Override
-       public void setAuthorCover(String author, String luid) {
+       public void setAuthorCover(String author, String luid) throws IOException {
                setAuthorCover(author, getCover(luid));
        }
 
index 8442aed1fd1663b8bb013ef8f26960e84b42e1fb..a6c68546882c0cdd64bda6c7592976d228bdfb4d 100644 (file)
@@ -7,12 +7,13 @@ import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.net.ssl.SSLException;
+
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.utils.Image;
 import be.nikiroo.utils.Progress;
-import be.nikiroo.utils.StringUtils;
 import be.nikiroo.utils.Version;
 import be.nikiroo.utils.serial.server.ConnectActionClientObject;
 
@@ -24,12 +25,71 @@ import be.nikiroo.utils.serial.server.ConnectActionClientObject;
  * @author niki
  */
 public class RemoteLibrary extends BasicLibrary {
+       interface RemoteAction {
+               public void action(ConnectActionClientObject action) throws Exception;
+       }
+
+       class RemoteConnectAction extends ConnectActionClientObject {
+               public RemoteConnectAction() throws IOException {
+                       super(host, port, key);
+               }
+
+               @Override
+               public Object send(Object data) throws IOException,
+                               NoSuchFieldException, NoSuchMethodException,
+                               ClassNotFoundException {
+                       Object rep = super.send(data);
+                       if (rep instanceof RemoteLibraryException) {
+                               RemoteLibraryException remoteEx = (RemoteLibraryException) rep;
+                               throw remoteEx.unwrapException();
+                       }
+
+                       return rep;
+               }
+       }
+
        private String host;
        private int port;
-       private final String md5;
+       private final String key;
+       private final String subkey;
+
+       // informative only (server will make the actual checks)
+       private boolean rw;
 
        /**
         * Create a {@link RemoteLibrary} linked to the given server.
+        * <p>
+        * Note that the key is structured:
+        * <tt><b><i>xxx</i></b>(|<b><i>yyy</i></b>|<b>wl</b>)(|<b>rw</b>)</tt>
+        * <p>
+        * Note that anything before the first pipe (<tt>|</tt>) character is
+        * considered to be the encryption key, anything after that character is
+        * called the subkey (including the other pipe characters and flags!).
+        * <p>
+        * This is important because the subkey (including the pipe characters and
+        * flags) must be present as-is in the server configuration file to be
+        * allowed.
+        * <ul>
+        * <li><b><i>xxx</i></b>: the encryption key used to communicate with the
+        * server</li>
+        * <li><b><i>yyy</i></b>: the secondary key</li>
+        * <li><b>rw</b>: flag to allow read and write access if it is not the
+        * default on this server</li>
+        * <li><b>wl</b>: flag to allow access to all the stories (bypassing the
+        * whitelist if it exists)</li>
+        * </ul>
+        * 
+        * Some examples:
+        * <ul>
+        * <li><b>my_key</b>: normal connection, will take the default server
+        * options</li>
+        * <li><b>my_key|agzyzz|wl</b>: will ask to bypass the white list (if it
+        * exists)</li>
+        * <li><b>my_key|agzyzz|rw</b>: will ask read-write access (if the default
+        * is read-only)</li>
+        * <li><b>my_key|agzyzz|wl|rw</b>: will ask both read-write access and white
+        * list bypass</li>
+        * </ul>
         * 
         * @param key
         *            the key that will allow us to exchange information with the
@@ -40,41 +100,67 @@ public class RemoteLibrary extends BasicLibrary {
         *            the port to contact it on
         */
        public RemoteLibrary(String key, String host, int port) {
-               this.md5 = StringUtils.getMd5Hash(key);
+               int index = -1;
+               if (key != null) {
+                       index = key.indexOf('|');
+               }
+
+               if (index >= 0) {
+                       this.key = key.substring(0, index);
+                       this.subkey = key.substring(index + 1);
+               } else {
+                       this.key = key;
+                       this.subkey = "";
+               }
+
                this.host = host;
                this.port = port;
        }
 
        @Override
        public String getLibraryName() {
-               return host + ":" + port;
+               return (rw ? "[READ-ONLY] " : "") + host + ":" + port;
        }
 
        @Override
        public Status getStatus() {
+               Instance.getTraceHandler().trace("Getting remote lib status...");
+               Status status = getStatusDo();
+               Instance.getTraceHandler().trace("Remote lib status: " + status);
+               return status;
+       }
+
+       private Status getStatusDo() {
                final Status[] result = new Status[1];
 
                result[0] = Status.INVALID;
 
-               ConnectActionClientObject action = null;
                try {
-                       action = new ConnectActionClientObject(host, port, true) {
+                       new RemoteConnectAction() {
                                @Override
                                public void action(Version serverVersion) throws Exception {
-                                       Object rep = send(new Object[] { md5, "PING" });
-                                       if ("PONG".equals(rep)) {
-                                               result[0] = Status.READY;
+                                       Object rep = send(new Object[] { subkey, "PING" });
+
+                                       if ("r/w".equals(rep)) {
+                                               rw = true;
+                                               result[0] = Status.READ_WRITE;
+                                       } else if ("r/o".equals(rep)) {
+                                               rw = false;
+                                               result[0] = Status.READ_ONLY;
                                        } else {
-                                               result[0] = Status.UNAUTORIZED;
+                                               result[0] = Status.UNAUTHORIZED;
                                        }
                                }
 
                                @Override
                                protected void onError(Exception e) {
-                                       result[0] = Status.UNAVAILABLE;
+                                       if (e instanceof SSLException) {
+                                               result[0] = Status.UNAUTHORIZED;
+                                       } else {
+                                               result[0] = Status.UNAVAILABLE;
+                                       }
                                }
-                       };
-
+                       }.connect();
                } catch (UnknownHostException e) {
                        result[0] = Status.INVALID;
                } catch (IllegalArgumentException e) {
@@ -83,118 +169,91 @@ public class RemoteLibrary extends BasicLibrary {
                        result[0] = Status.UNAVAILABLE;
                }
 
-               if (action != null) {
-                       try {
-                               action.connect();
-                       } catch (Exception e) {
-                               result[0] = Status.UNAVAILABLE;
-                       }
-               }
-
                return result[0];
        }
 
        @Override
-       public Image getCover(final String luid) {
+       public Image getCover(final String luid) throws IOException {
                final Image[] result = new Image[1];
 
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       Object rep = send(new Object[] { md5, "GET_COVER", luid });
-                                       result[0] = (Image) rep;
-                               }
-
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
-                               }
-                       }.connect();
-               } catch (Exception e) {
-                       Instance.getTraceHandler().error(e);
-               }
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               Object rep = action.send(new Object[] { subkey, "GET_COVER",
+                                               luid });
+                               result[0] = (Image) rep;
+                       }
+               });
 
                return result[0];
        }
 
        @Override
-       public Image getCustomSourceCover(final String source) {
+       public Image getCustomSourceCover(final String source) throws IOException {
                return getCustomCover(source, "SOURCE");
        }
 
        @Override
-       public Image getCustomAuthorCover(final String author) {
+       public Image getCustomAuthorCover(final String author) throws IOException {
                return getCustomCover(author, "AUTHOR");
        }
 
        // type: "SOURCE" or "AUTHOR"
-       private Image getCustomCover(final String source, final String type) {
+       private Image getCustomCover(final String source, final String type)
+                       throws IOException {
                final Image[] result = new Image[1];
 
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       Object rep = send(new Object[] { md5, "GET_CUSTOM_COVER",
-                                                       type, source });
-                                       result[0] = (Image) rep;
-                               }
-
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
-                               }
-                       }.connect();
-               } catch (Exception e) {
-                       Instance.getTraceHandler().error(e);
-               }
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               Object rep = action.send(new Object[] { subkey,
+                                               "GET_CUSTOM_COVER", type, source });
+                               result[0] = (Image) rep;
+                       }
+               });
 
                return result[0];
        }
 
        @Override
-       public synchronized Story getStory(final String luid, Progress pg) {
+       public synchronized Story getStory(final String luid, Progress pg)
+                       throws IOException {
                final Progress pgF = pg;
                final Story[] result = new Story[1];
 
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       Progress pg = pgF;
-                                       if (pg == null) {
-                                               pg = new Progress();
-                                       }
-
-                                       Object rep = send(new Object[] { md5, "GET_STORY", luid });
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               Progress pg = pgF;
+                               if (pg == null) {
+                                       pg = new Progress();
+                               }
 
-                                       MetaData meta = null;
-                                       if (rep instanceof MetaData) {
-                                               meta = (MetaData) rep;
-                                               if (meta.getWords() <= Integer.MAX_VALUE) {
-                                                       pg.setMinMax(0, (int) meta.getWords());
-                                               }
-                                       }
+                               Object rep = action.send(new Object[] { subkey, "GET_STORY",
+                                               luid });
 
-                                       List<Object> list = new ArrayList<Object>();
-                                       for (Object obj = send(null); obj != null; obj = send(null)) {
-                                               list.add(obj);
-                                               pg.add(1);
+                               MetaData meta = null;
+                               if (rep instanceof MetaData) {
+                                       meta = (MetaData) rep;
+                                       if (meta.getWords() <= Integer.MAX_VALUE) {
+                                               pg.setMinMax(0, (int) meta.getWords());
                                        }
-
-                                       result[0] = RemoteLibraryServer.rebuildStory(list);
-                                       pg.done();
                                }
 
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
+                               List<Object> list = new ArrayList<Object>();
+                               for (Object obj = action.send(null); obj != null; obj = action
+                                               .send(null)) {
+                                       list.add(obj);
+                                       pg.add(1);
                                }
-                       }.connect();
-               } catch (Exception e) {
-                       Instance.getTraceHandler().error(e);
-               }
+
+                               result[0] = RemoteLibraryServer.rebuildStory(list);
+                               pg.done();
+                       }
+               });
 
                return result[0];
        }
@@ -202,6 +261,7 @@ public class RemoteLibrary extends BasicLibrary {
        @Override
        public synchronized Story save(final Story story, final String luid,
                        Progress pg) throws IOException {
+
                final String[] luidSaved = new String[1];
                Progress pgSave = new Progress();
                Progress pgRefresh = new Progress();
@@ -215,32 +275,28 @@ public class RemoteLibrary extends BasicLibrary {
 
                final Progress pgF = pgSave;
 
-               new ConnectActionClientObject(host, port, true) {
+               connectRemoteAction(new RemoteAction() {
                        @Override
-                       public void action(Version serverVersion) throws Exception {
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
                                Progress pg = pgF;
                                if (story.getMeta().getWords() <= Integer.MAX_VALUE) {
                                        pg.setMinMax(0, (int) story.getMeta().getWords());
                                }
 
-                               send(new Object[] { md5, "SAVE_STORY", luid });
+                               action.send(new Object[] { subkey, "SAVE_STORY", luid });
 
                                List<Object> list = RemoteLibraryServer.breakStory(story);
                                for (Object obj : list) {
-                                       send(obj);
+                                       action.send(obj);
                                        pg.add(1);
                                }
 
-                               luidSaved[0] = (String) send(null);
+                               luidSaved[0] = (String) action.send(null);
 
                                pg.done();
                        }
-
-                       @Override
-                       protected void onError(Exception e) {
-                               Instance.getTraceHandler().error(e);
-                       }
-               }.connect();
+               });
 
                // because the meta changed:
                MetaData meta = getInfo(luidSaved[0]);
@@ -260,47 +316,38 @@ public class RemoteLibrary extends BasicLibrary {
 
        @Override
        public synchronized void delete(final String luid) throws IOException {
-               new ConnectActionClientObject(host, port, true) {
-                       @Override
-                       public void action(Version serverVersion) throws Exception {
-                               send(new Object[] { md5, "DELETE_STORY", luid });
-                       }
-
+               connectRemoteAction(new RemoteAction() {
                        @Override
-                       protected void onError(Exception e) {
-                               Instance.getTraceHandler().error(e);
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               action.send(new Object[] { subkey, "DELETE_STORY", luid });
                        }
-               }.connect();
+               });
        }
 
        @Override
-       public void setSourceCover(final String source, final String luid) {
+       public void setSourceCover(final String source, final String luid)
+                       throws IOException {
                setCover(source, luid, "SOURCE");
        }
 
        @Override
-       public void setAuthorCover(final String author, final String luid) {
+       public void setAuthorCover(final String author, final String luid)
+                       throws IOException {
                setCover(author, luid, "AUTHOR");
        }
 
        // type = "SOURCE" | "AUTHOR"
        private void setCover(final String value, final String luid,
-                       final String type) {
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       send(new Object[] { md5, "SET_COVER", type, value, luid });
-                               }
-
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
-                               }
-                       }.connect();
-               } catch (IOException e) {
-                       Instance.getTraceHandler().error(e);
-               }
+                       final String type) throws IOException {
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               action.send(new Object[] { subkey, "SET_COVER", type, value,
+                                               luid });
+                       }
+               });
        }
 
        @Override
@@ -326,35 +373,27 @@ public class RemoteLibrary extends BasicLibrary {
                final Progress pgF = pgImprt;
                final String[] luid = new String[1];
 
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       Progress pg = pgF;
-
-                                       Object rep = send(new Object[] { md5, "IMPORT",
-                                                       url.toString() });
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               Progress pg = pgF;
 
-                                       while (true) {
-                                               if (!RemoteLibraryServer.updateProgress(pg, rep)) {
-                                                       break;
-                                               }
+                               Object rep = action.send(new Object[] { subkey, "IMPORT",
+                                               url.toString() });
 
-                                               rep = send(null);
+                               while (true) {
+                                       if (!RemoteLibraryServer.updateProgress(pg, rep)) {
+                                               break;
                                        }
 
-                                       pg.done();
-                                       luid[0] = (String) rep;
+                                       rep = action.send(null);
                                }
 
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
-                               }
-                       }.connect();
-               } catch (IOException e) {
-                       Instance.getTraceHandler().error(e);
-               }
+                               pg.done();
+                               luid[0] = (String) rep;
+                       }
+               });
 
                if (luid[0] == null) {
                        throw new IOException("Remote failure");
@@ -372,33 +411,26 @@ public class RemoteLibrary extends BasicLibrary {
        protected synchronized void changeSTA(final String luid,
                        final String newSource, final String newTitle,
                        final String newAuthor, Progress pg) throws IOException {
-               final Progress pgF = pg == null ? new Progress() : pg;
 
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       Progress pg = pgF;
+               final Progress pgF = pg == null ? new Progress() : pg;
 
-                                       Object rep = send(new Object[] { md5, "CHANGE_STA", luid,
-                                                       newSource, newTitle, newAuthor });
-                                       while (true) {
-                                               if (!RemoteLibraryServer.updateProgress(pg, rep)) {
-                                                       break;
-                                               }
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               Progress pg = pgF;
 
-                                               rep = send(null);
+                               Object rep = action.send(new Object[] { subkey, "CHANGE_STA",
+                                               luid, newSource, newTitle, newAuthor });
+                               while (true) {
+                                       if (!RemoteLibraryServer.updateProgress(pg, rep)) {
+                                               break;
                                        }
-                               }
 
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
+                                       rep = action.send(null);
                                }
-                       }.connect();
-               } catch (IOException e) {
-                       Instance.getTraceHandler().error(e);
-               }
+                       }
+               });
        }
 
        @Override
@@ -409,27 +441,22 @@ public class RemoteLibrary extends BasicLibrary {
 
        /**
         * Stop the server.
+        * 
+        * @throws IOException
+        *             in case of I/O error (including bad key)
         */
-       public void exit() {
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       send(new Object[] { md5, "EXIT" });
-                               }
-
-                               @Override
-                               protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
-                               }
-                       }.connect();
-               } catch (IOException e) {
-                       Instance.getTraceHandler().error(e);
-               }
+       public void exit() throws IOException {
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               action.send(new Object[] { subkey, "EXIT" });
+                       }
+               });
        }
 
        @Override
-       public synchronized MetaData getInfo(String luid) {
+       public synchronized MetaData getInfo(String luid) throws IOException {
                List<MetaData> metas = getMetasList(luid, null);
                if (!metas.isEmpty()) {
                        return metas.get(0);
@@ -439,7 +466,7 @@ public class RemoteLibrary extends BasicLibrary {
        }
 
        @Override
-       protected List<MetaData> getMetas(Progress pg) {
+       protected List<MetaData> getMetas(Progress pg) throws IOException {
                return getMetasList("*", pg);
        }
 
@@ -483,50 +510,78 @@ public class RemoteLibrary extends BasicLibrary {
         * @param pg
         *            the optional progress
         * 
-        * 
         * @return the metas
+        * 
+        * @throws IOException
+        *             in case of I/O error or bad key (SSLException)
         */
-       private List<MetaData> getMetasList(final String luid, Progress pg) {
+       private List<MetaData> getMetasList(final String luid, Progress pg)
+                       throws IOException {
                final Progress pgF = pg;
                final List<MetaData> metas = new ArrayList<MetaData>();
 
-               try {
-                       new ConnectActionClientObject(host, port, true) {
-                               @Override
-                               public void action(Version serverVersion) throws Exception {
-                                       Progress pg = pgF;
-                                       if (pg == null) {
-                                               pg = new Progress();
-                                       }
-
-                                       Object rep = send(new Object[] { md5, "GET_METADATA", luid });
+               connectRemoteAction(new RemoteAction() {
+                       @Override
+                       public void action(ConnectActionClientObject action)
+                                       throws Exception {
+                               Progress pg = pgF;
+                               if (pg == null) {
+                                       pg = new Progress();
+                               }
 
-                                       while (true) {
-                                               if (!RemoteLibraryServer.updateProgress(pg, rep)) {
-                                                       break;
-                                               }
+                               Object rep = action.send(new Object[] { subkey, "GET_METADATA",
+                                               luid });
 
-                                               rep = send(null);
+                               while (true) {
+                                       if (!RemoteLibraryServer.updateProgress(pg, rep)) {
+                                               break;
                                        }
 
-                                       if (rep instanceof MetaData[]) {
-                                               for (MetaData meta : (MetaData[]) rep) {
-                                                       metas.add(meta);
-                                               }
-                                       } else if (rep != null) {
-                                               metas.add((MetaData) rep);
+                                       rep = action.send(null);
+                               }
+
+                               if (rep instanceof MetaData[]) {
+                                       for (MetaData meta : (MetaData[]) rep) {
+                                               metas.add(meta);
                                        }
+                               } else if (rep != null) {
+                                       metas.add((MetaData) rep);
+                               }
+                       }
+               });
+
+               return metas;
+       }
+
+       private void connectRemoteAction(final RemoteAction runAction)
+                       throws IOException {
+               final IOException[] err = new IOException[1];
+               try {
+                       final RemoteConnectAction[] array = new RemoteConnectAction[1];
+                       RemoteConnectAction ra = new RemoteConnectAction() {
+                               @Override
+                               public void action(Version serverVersion) throws Exception {
+                                       runAction.action(array[0]);
                                }
 
                                @Override
                                protected void onError(Exception e) {
-                                       Instance.getTraceHandler().error(e);
+                                       if (!(e instanceof IOException)) {
+                                               Instance.getTraceHandler().error(e);
+                                               return;
+                                       }
+
+                                       err[0] = (IOException) e;
                                }
-                       }.connect();
+                       };
+                       array[0] = ra;
+                       ra.connect();
                } catch (Exception e) {
-                       Instance.getTraceHandler().error(e);
+                       err[0] = (IOException) e;
                }
 
-               return metas;
+               if (err[0] != null) {
+                       throw err[0];
+               }
        }
 }
diff --git a/src/be/nikiroo/fanfix/library/RemoteLibraryException.java b/src/be/nikiroo/fanfix/library/RemoteLibraryException.java
new file mode 100644 (file)
index 0000000..4cbb631
--- /dev/null
@@ -0,0 +1,100 @@
+package be.nikiroo.fanfix.library;
+
+import java.io.IOException;
+
+/**
+ * Exceptions sent from remote to local.
+ * 
+ * @author niki
+ */
+public class RemoteLibraryException extends IOException {
+       private static final long serialVersionUID = 1L;
+
+       private boolean wrapped;
+
+       @SuppressWarnings("unused")
+       private RemoteLibraryException() {
+               // for serialization purposes
+       }
+
+       /**
+        * Wrap an {@link IOException} to allow it to pass across the network.
+        * 
+        * @param cause
+        *            the exception to wrap
+        * @param remote
+        *            this exception is used to send the contained
+        *            {@link IOException} to the other end of the network
+        */
+       public RemoteLibraryException(IOException cause, boolean remote) {
+               this(null, cause, remote);
+       }
+
+       /**
+        * Wrap an {@link IOException} to allow it to pass across the network.
+        * 
+        * @param message
+        *            the error message
+        * @param wrapped
+        *            this exception is used to send the contained
+        *            {@link IOException} to the other end of the network
+        */
+       public RemoteLibraryException(String message, boolean wrapped) {
+               this(message, null, wrapped);
+       }
+
+       /**
+        * Wrap an {@link IOException} to allow it to pass across the network.
+        * 
+        * @param message
+        *            the error message
+        * @param cause
+        *            the exception to wrap
+        * @param wrapped
+        *            this exception is used to send the contained
+        *            {@link IOException} to the other end of the network
+        */
+       public RemoteLibraryException(String message, IOException cause,
+                       boolean wrapped) {
+               super(message, cause);
+               this.wrapped = wrapped;
+       }
+
+       /**
+        * Return the actual exception we should return to the client code. It can
+        * be:
+        * <ul>
+        * <li>the <tt>cause</tt> if {@link RemoteLibraryException#isWrapped()} is
+        * TRUE</li>
+        * <li><tt>this</tt> if {@link RemoteLibraryException#isWrapped()} is FALSE
+        * (</li>
+        * <li><tt>this</tt> if the <tt>cause</tt> is NULL (so we never return NULL)
+        * </li>
+        * </ul>
+        * It is never NULL.
+        * 
+        * @return the unwrapped exception or <tt>this</tt>, never NULL
+        */
+       public synchronized IOException unwrapException() {
+               Throwable ex = super.getCause();
+               if (!isWrapped() || !(ex instanceof IOException)) {
+                       ex = this;
+               }
+
+               return (IOException) ex;
+       }
+
+       /**
+        * This exception is used to send the contained {@link IOException} to the
+        * other end of the network.
+        * <p>
+        * In other words, do not use <tt>this</tt> exception in client code when it
+        * has reached the other end of the network, but use its cause instead (see
+        * {@link RemoteLibraryException#unwrapException()}).
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isWrapped() {
+               return wrapped;
+       }
+}
index d73943831144add2b16bd4edfeaf35cd9c43d94d..43f61b096cbb6e8d452a47b13853a1a2fc15af7c 100644 (file)
@@ -4,9 +4,14 @@ import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLException;
 
 import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.Config;
 import be.nikiroo.fanfix.data.Chapter;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Paragraph;
@@ -19,45 +24,51 @@ import be.nikiroo.utils.serial.server.ConnectActionServerObject;
 import be.nikiroo.utils.serial.server.ServerObject;
 
 /**
- * Create a new remote server that will listen for order on the given port.
+ * Create a new remote server that will listen for orders on the given port.
+ * <p>
+ * The available commands are given as arrays of objects (first item is the
+ * command, the rest are the arguments).
  * <p>
- * The available commands are given as arrays of objects (first item is the key,
- * second is the command, the rest are the arguments).
+ * All the commands are always prefixed by the subkey (which can be EMPTY if
+ * none).
  * <p>
- * The md5 is always a String (the MD5 hash of the access key), the commands are
- * also Strings; the parameters vary depending upon the command.
  * <ul>
- * <li>[md5] PING: will return PONG if the key is accepted</li>
- * <li>[md5] GET_METADATA *: will return the metadata of all the stories in the
- * library (array)</li>
- * *
- * <li>[md5] GET_METADATA [luid]: will return the metadata of the story of LUID
- * luid</li>
- * <li>[md5] GET_STORY [luid]: will return the given story if it exists (or NULL
- * if not)</li>
- * <li>[md5] SAVE_STORY [luid]: save the story (that must be sent just after the
+ * <li>PING: will return the mode if the key is accepted (mode can be: "r/o" or
+ * "r/w")</li>
+ * <li>GET_METADATA *: will return the metadata of all the stories in the
+ * library (array)</li> *
+ * <li>GET_METADATA [luid]: will return the metadata of the story of LUID luid</li>
+ * <li>GET_STORY [luid]: will return the given story if it exists (or NULL if
+ * not)</li>
+ * <li>SAVE_STORY [luid]: save the story (that must be sent just after the
  * command) with the given LUID, then return the LUID</li>
- * <li>[md5] IMPORT [url]: save the story found at the given URL, then return
- * the LUID</li>
- * <li>[md5] DELETE_STORY [luid]: delete the story of LUID luid</li>
- * <li>[md5] GET_COVER [luid]: return the cover of the story</li>
- * <li>[md5] GET_CUSTOM_COVER ["SOURCE"|"AUTHOR"] [source]: return the cover for
- * this source/author</li>
- * <li>[md5] SET_COVER ["SOURCE"|"AUTHOR"] [value] [luid]: set the default cover
- * for the given source/author to the cover of the story denoted by luid</li>
- * <li>[md5] CHANGE_SOURCE [luid] [new source]: change the source of the story
- * of LUID luid</li>
- * <li>[md5] EXIT: stop the server</li>
+ * <li>IMPORT [url]: save the story found at the given URL, then return the LUID
+ * </li>
+ * <li>DELETE_STORY [luid]: delete the story of LUID luid</li>
+ * <li>GET_COVER [luid]: return the cover of the story</li>
+ * <li>GET_CUSTOM_COVER ["SOURCE"|"AUTHOR"] [source]: return the cover for this
+ * source/author</li>
+ * <li>SET_COVER ["SOURCE"|"AUTHOR"] [value] [luid]: set the default cover for
+ * the given source/author to the cover of the story denoted by luid</li>
+ * <li>CHANGE_SOURCE [luid] [new source]: change the source of the story of LUID
+ * luid</li>
+ * <li>EXIT: stop the server</li>
  * </ul>
  * 
  * @author niki
  */
 public class RemoteLibraryServer extends ServerObject {
-       private final String md5;
+       private Map<Long, String> commands = new HashMap<Long, String>();
+       private Map<Long, Long> times = new HashMap<Long, Long>();
+       private Map<Long, Boolean> wls = new HashMap<Long, Boolean>();
+       private Map<Long, Boolean> rws = new HashMap<Long, Boolean>();
 
        /**
         * Create a new remote server (will not be active until
         * {@link RemoteLibraryServer#start()} is called).
+        * <p>
+        * Note: the key we use here is the encryption key (it must not contain a
+        * subkey).
         * 
         * @param key
         *            the key that will restrict access to this server
@@ -68,22 +79,28 @@ public class RemoteLibraryServer extends ServerObject {
         *             in case of I/O error
         */
        public RemoteLibraryServer(String key, int port) throws IOException {
-               super("Fanfix remote library", port, true);
-               this.md5 = StringUtils.getMd5Hash(key);
-
+               super("Fanfix remote library", port, key);
                setTraceHandler(Instance.getTraceHandler());
        }
 
        @Override
        protected Object onRequest(ConnectActionServerObject action,
-                       Version clientVersion, Object data) throws Exception {
-               String md5 = "";
+                       Version clientVersion, Object data, long id) throws Exception {
+               long start = new Date().getTime();
+
+               // defaults are positive (as previous versions without the feature)
+               boolean rw = true;
+               boolean wl = true;
+
+               String subkey = "";
                String command = "";
                Object[] args = new Object[0];
                if (data instanceof Object[]) {
                        Object[] dataArray = (Object[]) data;
-                       if (dataArray.length >= 2) {
-                               md5 = "" + dataArray[0];
+                       if (dataArray.length > 0) {
+                               subkey = "" + dataArray[0];
+                       }
+                       if (dataArray.length > 1) {
                                command = "" + dataArray[1];
 
                                args = new Object[dataArray.length - 2];
@@ -93,38 +110,99 @@ public class RemoteLibraryServer extends ServerObject {
                        }
                }
 
-               String trace = "[ " + command + "] ";
-               for (Object arg : args) {
-                       trace += arg + " ";
+               List<String> whitelist = Instance.getConfig().getList(
+                               Config.SERVER_WHITELIST);
+               if (whitelist == null) {
+                       whitelist = new ArrayList<String>();
                }
-               getTraceHandler().trace(trace);
 
-               if (!md5.equals(this.md5)) {
-                       getTraceHandler().trace("Key rejected.");
-                       return null;
+               if (whitelist.isEmpty()) {
+                       wl = false;
                }
 
-               long start = new Date().getTime();
-               Object rep = doRequest(action, command, args);
+               rw = Instance.getConfig().getBoolean(Config.SERVER_RW, rw);
+               if (!subkey.isEmpty()) {
+                       List<String> allowed = Instance.getConfig().getList(
+                                       Config.SERVER_ALLOWED_SUBKEYS);
+                       if (allowed.contains(subkey)) {
+                               if ((subkey + "|").contains("|rw|")) {
+                                       rw = true;
+                               }
+                               if ((subkey + "|").contains("|wl|")) {
+                                       wl = false; // |wl| = bypass whitelist
+                                       whitelist = new ArrayList<String>();
+                               }
+                       }
+               }
+
+               String mode = display(wl, rw);
+
+               String trace = mode + "[ " + command + "] ";
+               for (Object arg : args) {
+                       trace += arg + " ";
+               }
+               long now = System.currentTimeMillis();
+               System.out.println(StringUtils.fromTime(now) + ": " + trace);
+
+               Object rep = null;
+               try {
+                       rep = doRequest(action, command, args, rw, whitelist);
+               } catch (IOException e) {
+                       rep = new RemoteLibraryException(e, true);
+               }
 
-               getTraceHandler().trace(
-                               String.format("[>%s]: %d ms", command,
-                                               (new Date().getTime() - start)));
+               commands.put(id, command);
+               wls.put(id, wl);
+               rws.put(id, rw);
+               times.put(id, (new Date().getTime() - start));
 
                return rep;
        }
 
+       private String display(boolean whitelist, boolean rw) {
+               String mode = "";
+               if (!rw) {
+                       mode += "RO: ";
+               }
+               if (whitelist) {
+                       mode += "WL: ";
+               }
+
+               return mode;
+       }
+
+       @Override
+       protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
+               boolean whitelist = wls.get(id);
+               boolean rw = rws.get(id);
+               wls.remove(id);
+               rws.remove(id);
+
+               String rec = StringUtils.formatNumber(bytesReceived) + "b";
+               String sent = StringUtils.formatNumber(bytesSent) + "b";
+               long now = System.currentTimeMillis();
+               System.out.println(StringUtils.fromTime(now)
+                               + ": "
+                               + String.format("%s[>%s]: (%s sent, %s rec) in %d ms",
+                                               display(whitelist, rw), commands.get(id), sent, rec,
+                                               times.get(id)));
+
+               commands.remove(id);
+               times.remove(id);
+       }
+
        private Object doRequest(ConnectActionServerObject action, String command,
-                       Object[] args) throws NoSuchFieldException, NoSuchMethodException,
+                       Object[] args, boolean rw, List<String> whitelist)
+                       throws NoSuchFieldException, NoSuchMethodException,
                        ClassNotFoundException, IOException {
                if ("PING".equals(command)) {
-                       return "PONG";
+                       return rw ? "r/w" : "r/o";
                } else if ("GET_METADATA".equals(command)) {
+                       List<MetaData> metas = new ArrayList<MetaData>();
+
                        if ("*".equals(args[0])) {
                                Progress pg = createPgForwarder(action);
 
-                               List<MetaData> metas = new ArrayList<MetaData>();
-
                                for (MetaData meta : Instance.getLibrary().getMetas(pg)) {
                                        MetaData light;
                                        if (meta.getCover() == null) {
@@ -138,13 +216,41 @@ public class RemoteLibraryServer extends ServerObject {
                                }
 
                                forcePgDoneSent(pg);
-                               return metas.toArray(new MetaData[] {});
+                       } else {
+                               MetaData meta = Instance.getLibrary().getInfo((String) args[0]);
+                               MetaData light;
+                               if (meta.getCover() == null) {
+                                       light = meta;
+                               } else {
+                                       light = meta.clone();
+                                       light.setCover(null);
+                               }
+
+                               metas.add(light);
                        }
 
-                       return new MetaData[] { Instance.getLibrary().getInfo(
-                                       (String) args[0]) };
+                       if (!whitelist.isEmpty()) {
+                               for (int i = 0; i < metas.size(); i++) {
+                                       if (!whitelist.contains(metas.get(i).getSource())) {
+                                               metas.remove(i);
+                                               i--;
+                                       }
+                               }
+                       }
+
+                       return metas.toArray(new MetaData[0]);
                } else if ("GET_STORY".equals(command)) {
                        MetaData meta = Instance.getLibrary().getInfo((String) args[0]);
+                       if (meta == null) {
+                               return null;
+                       }
+
+                       if (!whitelist.isEmpty()) {
+                               if (!whitelist.contains(meta.getSource())) {
+                                       return null;
+                               }
+                       }
+
                        meta = meta.clone();
                        meta.setCover(null);
 
@@ -158,6 +264,11 @@ public class RemoteLibraryServer extends ServerObject {
                                action.rec();
                        }
                } else if ("SAVE_STORY".equals(command)) {
+                       if (!rw) {
+                               throw new RemoteLibraryException("Read-Only remote library: "
+                                               + args[0], false);
+                       }
+
                        List<Object> list = new ArrayList<Object>();
 
                        action.send(null);
@@ -172,12 +283,22 @@ public class RemoteLibraryServer extends ServerObject {
                        Instance.getLibrary().save(story, (String) args[0], null);
                        return story.getMeta().getLuid();
                } else if ("IMPORT".equals(command)) {
+                       if (!rw) {
+                               throw new RemoteLibraryException("Read-Only remote library: "
+                                               + args[0], false);
+                       }
+
                        Progress pg = createPgForwarder(action);
                        Story story = Instance.getLibrary().imprt(
                                        new URL((String) args[0]), pg);
                        forcePgDoneSent(pg);
                        return story.getMeta().getLuid();
                } else if ("DELETE_STORY".equals(command)) {
+                       if (!rw) {
+                               throw new RemoteLibraryException("Read-Only remote library: "
+                                               + args[0], false);
+                       }
+
                        Instance.getLibrary().delete((String) args[0]);
                } else if ("GET_COVER".equals(command)) {
                        return Instance.getLibrary().getCover((String) args[0]);
@@ -192,6 +313,11 @@ public class RemoteLibraryServer extends ServerObject {
                                return null;
                        }
                } else if ("SET_COVER".equals(command)) {
+                       if (!rw) {
+                               throw new RemoteLibraryException("Read-Only remote library: "
+                                               + args[0] + ", " + args[1], false);
+                       }
+
                        if ("SOURCE".equals(args[0])) {
                                Instance.getLibrary().setSourceCover((String) args[1],
                                                (String) args[2]);
@@ -200,11 +326,21 @@ public class RemoteLibraryServer extends ServerObject {
                                                (String) args[2]);
                        }
                } else if ("CHANGE_STA".equals(command)) {
+                       if (!rw) {
+                               throw new RemoteLibraryException("Read-Only remote library: "
+                                               + args[0] + ", " + args[1], false);
+                       }
+
                        Progress pg = createPgForwarder(action);
                        Instance.getLibrary().changeSTA((String) args[0], (String) args[1],
                                        (String) args[2], (String) args[3], pg);
                        forcePgDoneSent(pg);
                } else if ("EXIT".equals(command)) {
+                       if (!rw) {
+                               throw new RemoteLibraryException(
+                                               "Read-Only remote library: EXIT", false);
+                       }
+
                        stop(0, false);
                }
 
@@ -213,7 +349,13 @@ public class RemoteLibraryServer extends ServerObject {
 
        @Override
        protected void onError(Exception e) {
-               getTraceHandler().error(e);
+               if (e instanceof SSLException) {
+                       long now = System.currentTimeMillis();
+                       System.out.println(StringUtils.fromTime(now) + ": "
+                                       + "[Client connection refused (bad key)]");
+               } else {
+                       getTraceHandler().error(e);
+               }
        }
 
        /**
@@ -308,8 +450,7 @@ public class RemoteLibraryServer extends ServerObject {
         * 
         * @return the {@link Progress}
         */
-       private static Progress createPgForwarder(
-                       final ConnectActionServerObject action) {
+       private Progress createPgForwarder(final ConnectActionServerObject action) {
                final Boolean[] isDoneForwarded = new Boolean[] { false };
                final Progress pg = new Progress() {
                        @Override
@@ -342,7 +483,7 @@ public class RemoteLibraryServer extends ServerObject {
                                                action.send(new Integer[] { min, max, relativeProgress });
                                                action.rec();
                                        } catch (Exception e) {
-                                               Instance.getTraceHandler().error(e);
+                                               getTraceHandler().error(e);
                                        }
 
                                        lastTime[0] = new Date().getTime();
@@ -356,14 +497,14 @@ public class RemoteLibraryServer extends ServerObject {
        }
 
        // with 30 seconds timeout
-       private static void forcePgDoneSent(Progress pg) {
+       private void forcePgDoneSent(Progress pg) {
                long start = new Date().getTime();
                pg.done();
                while (!pg.isDone() && new Date().getTime() - start < 30000) {
                        try {
                                Thread.sleep(100);
                        } catch (InterruptedException e) {
-                               Instance.getTraceHandler().error(e);
+                               getTraceHandler().error(e);
                        }
                }
        }
index 6929cac7141e865f9fea140482e062aa05c31d8b..cfd9cbef050ad9e0c9aeaa57877e5abf8d465b02 100644 (file)
@@ -4,7 +4,7 @@
  * files that you can read anywhere.
  * <p>
  * It has support for a {@link be.nikiroo.fanfix.library.BasicLibrary} system, 
- * too.
+ * too, and can even offer its services over the network.
  * 
  * @author niki
  */
index c2a650cf73af2d809d3c609f3bf957c246dbc312..61769c01f87951c5546ae59cc12948e88c1ebe9e 100644 (file)
@@ -54,7 +54,7 @@ public abstract class BasicReader implements Reader {
        }
 
        @Override
-       public synchronized Story getStory(Progress pg) {
+       public synchronized Story getStory(Progress pg) throws IOException {
                if (story == null) {
                        story = getLibrary().getStory(meta.getLuid(), pg);
                }
@@ -227,14 +227,19 @@ public abstract class BasicReader implements Reader {
                        tags.append(tag);
                }
 
+               // TODO: i18n
                metaDesc.put("Author", meta.getAuthor());
                metaDesc.put("Publication date", formatDate(meta.getDate()));
                metaDesc.put("Published on", meta.getPublisher());
                metaDesc.put("URL", meta.getUrl());
+               String count = "";
+               if (meta.getWords() > 0) {
+                       count = StringUtils.formatNumber(meta.getWords());
+               }
                if (meta.isImageDocument()) {
-                       metaDesc.put("Number of images", format(meta.getWords()));
+                       metaDesc.put("Number of images", count);
                } else {
-                       metaDesc.put("Number of words", format(meta.getWords()));
+                       metaDesc.put("Number of words", count);
                }
                metaDesc.put("Source", meta.getSource());
                metaDesc.put("Subject", meta.getSubject());
@@ -351,46 +356,32 @@ public abstract class BasicReader implements Reader {
                }
        }
 
-       static private String format(long value) {
-               String display = "";
-               String suffix = "";
-
-               if (value > 4000) {
-                       value = value / 1000;
-                       suffix = "k";
-               }
-
-               while (value > 0) {
-                       if (!display.isEmpty()) {
-                               display = "." + display;
-                       }
-                       display = (value % 1000) + display;
-                       value = value / 1000;
-               }
-
-               return display + suffix;
-       }
-
        static private String formatDate(String date) {
                long ms = 0;
 
-               try {
-                       ms = StringUtils.toTime(date);
-               } catch (ParseException e) {
-               }
-
-               if (ms <= 0) {
-                       SimpleDateFormat sdf = new SimpleDateFormat(
-                                       "yyyy-MM-dd'T'HH:mm:ssSSS");
+               if (date != null && !date.isEmpty()) {
                        try {
-                               ms = sdf.parse(date).getTime();
+                               ms = StringUtils.toTime(date);
                        } catch (ParseException e) {
                        }
+
+                       if (ms <= 0) {
+                               SimpleDateFormat sdf = new SimpleDateFormat(
+                                               "yyyy-MM-dd'T'HH:mm:ssSSS");
+                               try {
+                                       ms = sdf.parse(date).getTime();
+                               } catch (ParseException e) {
+                               }
+                       }
+
+                       if (ms > 0) {
+                               SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                               return sdf.format(new Date(ms));
+                       }
                }
 
-               if (ms > 0) {
-                       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-                       return sdf.format(new Date(ms));
+               if (date == null) {
+                       date = "";
                }
 
                // :(
index b001e304842f4956095050160d5bf65b8795d3f3..3ecf2470021b4f6d259db9ee8b80eac9ff8a266a 100644 (file)
@@ -6,6 +6,7 @@ import java.net.URL;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.BasicLibrary;
+import be.nikiroo.fanfix.supported.SupportType;
 import be.nikiroo.utils.Progress;
 
 /**
@@ -70,8 +71,12 @@ public interface Reader {
         *            the optional progress
         * 
         * @return the {@link Story}
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        * 
         */
-       public Story getStory(Progress pg);
+       public Story getStory(Progress pg) throws IOException;
 
        /**
         * The {@link BasicLibrary} to load the stories from (by default, takes the
@@ -166,8 +171,81 @@ public interface Reader {
         * @param source
         *            the type of {@link Story} to take into account, or NULL for
         *            all
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public void browse(String source) throws IOException;
+
+       /**
+        * Display all supports that allow search operations.
+        * 
+        * @param sync
+        *            execute the process synchronously (wait until it is terminated
+        *            before returning)
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public void search(boolean sync) throws IOException;
+
+       /**
+        * Search for the given terms and find stories that correspond if possible.
+        * 
+        * @param searchOn
+        *            the website to search on
+        * @param keywords
+        *            the words to search for (cannot be NULL)
+        * @param page
+        *            the page of results to show (0 = request the maximum number of
+        *            pages, pages start at 1)
+        * @param item
+        *            the item to select (0 = do not select a specific item but show
+        *            all the page, items start at 1)
+        * @param sync
+        *            execute the process synchronously (wait until it is terminated
+        *            before returning)
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public void search(SupportType searchOn, String keywords, int page,
+                       int item, boolean sync) throws IOException;
+
+       /**
+        * Search based upon a hierarchy of tags, or search for (sub)tags.
+        * <p>
+        * We use the tags <tt>DisplayName</tt>.
+        * <p>
+        * If no tag is given, the main tags will be shown.
+        * <p>
+        * If a non-leaf tag is given, the subtags will be shown.
+        * <p>
+        * If a leaf tag is given (or a full hierarchy ending with a leaf tag),
+        * stories will be shown.
+        * <p>
+        * You can select the story you want with the <tt>item</tt> number.
+        * 
+        * @param searchOn
+        *            the website to search on
+        * @param page
+        *            the page of results to show (0 = request the maximum number of
+        *            pages, pages <b>start at 1</b>)
+        * @param item
+        *            the item to select (0 = do not select a specific item but show
+        *            all the page, items <b>start at 1</b>)
+        * @param sync
+        *            execute the process synchronously (wait until it is terminated
+        *            before returning)
+        * @param tags
+        *            the tags indices to search for (this is a tag
+        *            <b>hierarchy</b>, <b>NOT</b> a multiple tags choice)
+        * 
+        * @throws IOException
+        *             in case of I/O error
         */
-       public void browse(String source);
+       public void searchTag(SupportType searchOn, int page, int item,
+                       boolean sync, Integer... tags) throws IOException;
 
        /**
         * Open the {@link Story} with an external reader (the program should be
diff --git a/src/be/nikiroo/fanfix/reader/android/AndroidReader.java b/src/be/nikiroo/fanfix/reader/android/AndroidReader.java
deleted file mode 100644 (file)
index 7dcdd04..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-package be.nikiroo.fanfix.reader.android;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-
-import java.io.File;
-import java.io.IOException;
-
-import be.nikiroo.fanfix.reader.BasicReader;
-
-public class AndroidReader extends BasicReader {
-       private Activity app;
-
-       /**
-        * Do not use.
-        */
-       private AndroidReader() {
-               // Required for reflection
-       }
-
-       public AndroidReader(Activity app) {
-               this.app = app;
-       }
-
-       @Override
-       public void read(boolean sync) throws IOException {
-       }
-
-       @Override
-       public void browse(String source) {
-       }
-
-       @Override
-       protected void start(File target, String program, boolean sync) throws IOException {
-               if (program == null) {
-                       try {
-                               Intent[] intents = new Intent[] { //
-                               new Intent(Intent.ACTION_VIEW), //
-                                               new Intent(Intent.ACTION_OPEN_DOCUMENT) //
-                               };
-
-                               for (Intent intent : intents) {
-                                       intent.setDataAndType(Uri.parse(target.toURI().toString()),
-                                                       "application/x-cbz");
-                               }
-
-                               Intent chooserIntent = Intent.createChooser(intents[0],
-                                               "Open CBZ in...");
-
-                               // chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
-                               // intents);
-
-                               app.startActivity(chooserIntent);
-                       } catch (UnsupportedOperationException e) {
-                               super.start(target, program, sync);
-                       }
-               } else {
-                       super.start(target, program, sync);
-               }
-       }
-}
diff --git a/src/be/nikiroo/fanfix/reader/android/AndroidReaderActivity.java b/src/be/nikiroo/fanfix/reader/android/AndroidReaderActivity.java
deleted file mode 100644 (file)
index 290b5ba..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-package be.nikiroo.fanfix.reader.android;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.FragmentTransaction;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.os.Environment;
-import android.text.InputType;
-import android.view.View;
-import android.widget.EditText;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-
-import be.nikiroo.fanfix.Instance;
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.reader.BasicReader;
-import be.nikiroo.fanfix.reader.Reader;
-import be.nikiroo.utils.TraceHandler;
-
-public class AndroidReaderActivity extends Activity implements
-               AndroidReaderBook.OnFragmentInteractionListener {
-       private static Reader reader = null;
-
-       @Override
-       protected void onCreate(Bundle savedInstanceState) {
-               reader = config();
-               super.onCreate(savedInstanceState);
-               setContentView(R.layout.activity_main);
-       }
-
-       @Override
-       protected void onStart() {
-               super.onStart();
-               refresh();
-       }
-
-       private void refresh() {
-               AndroidReaderGroup group = new AndroidReaderGroup();
-
-               FragmentTransaction trans = getFragmentManager().beginTransaction();
-               trans.replace(R.id.Main_pnlStories, group);
-               trans.commit();
-               getFragmentManager().executePendingTransactions();
-
-               group.fill(reader.getLibrary().getList(), reader);
-       }
-
-       public void onAdd(View view) {
-               final View root = findViewById(R.id.Main);
-
-               ask(this,
-                               "Import new story",
-                               "Enter the story URL (the program will then download it -- the interface will not be usable until it is downloaded",
-                               "Download", new AnswerListener() {
-                                       @Override
-                                       public void onAnswer(final String answer) {
-                                               root.setEnabled(false);
-                                               new Thread(new Runnable() {
-                                                       @Override
-                                                       public void run() {
-                                                               try {
-                                                                       URL url = new URL(answer);
-                                                                       reader.getLibrary().imprt(url, null);
-                                                               } catch (Throwable e) {
-                                                                       // TODO: show error message correctly
-                                                                       String mess = "";
-                                                                       for (String tab = ""; e != null
-                                                                                       && e != e.getCause(); e = e
-                                                                                       .getCause()) {
-                                                                               mess += tab + "["
-                                                                                               + e.getClass().getSimpleName()
-                                                                                               + "] " + e.getMessage() + "\n";
-                                                                               tab += "\t";
-                                                                       }
-
-                                                                       final String messf = mess;
-                                                                       AndroidReaderActivity.this
-                                                                                       .runOnUiThread(new Runnable() {
-                                                                                               @Override
-                                                                                               public void run() {
-                                                                                                       ask(AndroidReaderActivity.this,
-                                                                                                                       "Error",
-                                                                                                                       "Cannot import URL: \n"
-                                                                                                                                       + messf,
-                                                                                                                       "OK", null);
-                                                                                               }
-                                                                                       });
-
-                                                               }
-
-                                                               AndroidReaderActivity.this
-                                                                               .runOnUiThread(new Runnable() {
-                                                                                       @Override
-                                                                                       public void run() {
-                                                                                               refresh();
-                                                                                               root.setEnabled(true);
-                                                                                       }
-                                                                               });
-                                                       }
-                                               }).start();
-                                       }
-                               });
-
-               /*
-                * Intent intent = new Intent(AndroidReaderActivity.this, SayIt.class);
-                * intent.putExtra(SayIt.MESSAGE, message); startActivity(intent);
-                */
-       }
-
-       @Override
-       public void onFragmentInteraction(MetaData meta) {
-               AndroidReader reader = new AndroidReader(this);
-               try {
-                       reader.openExternal(Instance.getLibrary(), meta.getLuid());
-               } catch (IOException e) {
-                       e.printStackTrace();
-               }
-       }
-
-       private Reader config() {
-               if (reader != null) {
-                       return reader;
-               }
-
-               String internal = getExternalFilesDir(null).toString();
-               File user = Environment
-                               .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
-
-               try {
-                       File parent = user.getParentFile();
-                       if (parent.exists() || parent.mkdirs()) {
-                               File test = new File(parent, "test");
-                               if (test.exists() || (test.createNewFile() && test.delete())) {
-                                       user = parent;
-                               }
-                       }
-               } catch (Exception e) {
-                       // Fall back to Documents/Books
-               }
-
-               System.setProperty("DEBUG", "1");
-               System.setProperty("fanfix.home", internal);
-               System.setProperty("fanfix.libdir", new File(user, "Books").toString());
-
-               Instance.resetConfig(false);
-               Instance.setTraceHandler(new TraceHandler(true, true, 2));
-
-               BasicReader.setDefaultReaderType(Reader.ReaderType.ANDROID);
-               return BasicReader.getReader();
-       }
-
-       public static void ask(Context context, String title, String message,
-                       String okMessage, final AnswerListener listener) {
-               final EditText input = new EditText(context);
-               input.setFocusable(true);
-               input.setInputType(InputType.TYPE_CLASS_TEXT);
-
-               AlertDialog.Builder alert = new AlertDialog.Builder(context);
-               alert.setTitle(title);
-               alert.setMessage(message);
-               alert.setCancelable(true);
-               alert.setView(input);
-
-               if (listener != null) {
-                       alert.setPositiveButton(okMessage,
-                                       new DialogInterface.OnClickListener() {
-                                               @Override
-                                               public void onClick(DialogInterface dialog, int which) {
-                                                       listener.onAnswer(input.getText().toString());
-                                               }
-                                       });
-
-                       alert.setOnCancelListener(new DialogInterface.OnCancelListener() {
-                               @Override
-                               public void onCancel(DialogInterface dialog) {
-                                       listener.onAnswer(null);
-                               }
-                       });
-               }
-
-               alert.show();
-       }
-
-       private interface AnswerListener {
-               public void onAnswer(String answer);
-       }
-}
\ No newline at end of file
diff --git a/src/be/nikiroo/fanfix/reader/android/AndroidReaderBook.java b/src/be/nikiroo/fanfix/reader/android/AndroidReaderBook.java
deleted file mode 100644 (file)
index 700a566..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-package be.nikiroo.fanfix.reader.android;
-
-import android.app.Activity;
-import android.app.Fragment;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import java.io.IOException;
-
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.reader.Reader;
-import be.nikiroo.utils.Image;
-import be.nikiroo.utils.android.ImageUtilsAndroid;
-
-public class AndroidReaderBook extends Fragment {
-       private OnFragmentInteractionListener listener;
-
-       /**
-        * This interface must be implemented by activities that contain this
-        * fragment to allow an interaction in this fragment to be communicated to
-        * the activity and potentially other fragments contained in that activity.
-        * <p>
-        * See the Android Training lesson <a href=
-        * "http://developer.android.com/training/basics/fragments/communicating.html"
-        * >Communicating with Other Fragments</a> for more information.
-        */
-       public interface OnFragmentInteractionListener {
-               void onFragmentInteraction(MetaData meta);
-       }
-
-       public AndroidReaderBook() {
-               // Required empty public constructor
-       }
-
-       @Override
-       public View onCreateView(LayoutInflater inflater, ViewGroup container,
-                       Bundle savedInstanceState) {
-               return inflater.inflate(R.layout.fragment_android_reader_book,
-                               container, false);
-       }
-
-       @Override
-       public void onAttach(Activity context) {
-               super.onAttach(context);
-               if (context instanceof OnFragmentInteractionListener) {
-                       listener = (OnFragmentInteractionListener) context;
-               }
-       }
-
-       @Override
-       public void onDetach() {
-               super.onDetach();
-               listener = null;
-       }
-
-       public void fill(final MetaData meta, final Reader reader) {
-               ViewHolder viewHolder = new ViewHolder(getView());
-
-               viewHolder.title.setText(meta.getTitle());
-               viewHolder.author.setText(meta.getAuthor());
-               viewHolder.frame.setClickable(true);
-               viewHolder.frame.setFocusable(true);
-               viewHolder.frame.setOnClickListener(new View.OnClickListener() {
-                       @Override
-                       public void onClick(View v) {
-                               OnFragmentInteractionListener llistener = listener;
-                               if (llistener != null) {
-                                       llistener.onFragmentInteraction(meta);
-                               }
-                       }
-               });
-
-               new AsyncTask<MetaData, Void, Image>() {
-                       @Override
-                       protected Image doInBackground(MetaData[] metas) {
-                               if (metas[0].getCover() != null) {
-                                       return metas[0].getCover();
-                               }
-
-                               return reader.getLibrary().getCover(metas[0].getLuid());
-                       }
-
-                       @Override
-                       protected void onPostExecute(Image coverImage) {
-                               ViewHolder viewHolder = new ViewHolder(getView());
-
-                               try {
-                                       if (coverImage != null) {
-                                               Bitmap coverBitmap = ImageUtilsAndroid
-                                                               .fromImage(coverImage);
-                                               coverBitmap = Bitmap.createScaledBitmap(coverBitmap,
-                                                               128, 128, true);
-                                               viewHolder.cover.setImageBitmap(coverBitmap);
-                                       }
-                               } catch (IOException e) {
-                                       e.printStackTrace();
-                               }
-                       }
-               }.execute(meta);
-       }
-
-       private class ViewHolder {
-               public FrameLayout frame;
-               public TextView title;
-               public TextView author;
-               public ImageView cover;
-
-               public ViewHolder(View book) {
-                       frame = book.findViewById(R.id.Book);
-                       title = book.findViewById(R.id.Book_lblTitle);
-                       author = book.findViewById(R.id.Book_lblAuthor);
-                       cover = book.findViewById(R.id.Book_imgCover);
-               }
-       }
-}
diff --git a/src/be/nikiroo/fanfix/reader/android/AndroidReaderGroup.java b/src/be/nikiroo/fanfix/reader/android/AndroidReaderGroup.java
deleted file mode 100644 (file)
index 2d48199..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-package be.nikiroo.fanfix.reader.android;
-
-import android.app.Fragment;
-import android.app.FragmentTransaction;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.ListView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import be.nikiroo.fanfix.data.MetaData;
-import be.nikiroo.fanfix.reader.Reader;
-
-/**
- * A simple {@link Fragment} subclass. Activities that contain this fragment
- * must implement the {@link AndroidReaderGroup.OnFragmentInteractionListener}
- * interface to handle interaction events.
- */
-public class AndroidReaderGroup extends Fragment {
-       private OnFragmentInteractionListener listener;
-       private Map<View, AndroidReaderBook> books = new HashMap<View, AndroidReaderBook>();
-
-       public interface OnFragmentInteractionListener {
-               void onFragmentInteraction(MetaData meta);
-       }
-
-       public AndroidReaderGroup() {
-               // Required empty public constructor
-       }
-
-       @Override
-       public View onCreateView(LayoutInflater inflater, ViewGroup container,
-                       Bundle savedInstanceState) {
-               return inflater.inflate(R.layout.fragment_android_reader_group,
-                               container, false);
-       }
-
-       @Override
-       public void onAttach(Context context) {
-               super.onAttach(context);
-               if (context instanceof OnFragmentInteractionListener) {
-                       listener = (OnFragmentInteractionListener) context;
-               }
-       }
-
-       @Override
-       public void onDetach() {
-               super.onDetach();
-               listener = null;
-       }
-
-       public void fill(final List<MetaData> metas, final Reader reader) {
-               final List<MetaData> datas = new ArrayList<MetaData>(metas);
-
-               ListView list = getView().findViewById(R.id.Group_root);
-               list.setAdapter(new BaseAdapter() {
-                       @Override
-                       public int getCount() {
-                               return datas.size();
-                       }
-
-                       @Override
-                       public long getItemId(int position) {
-                               return -1; // TODO: what is a "row id" in this context?
-                       }
-
-                       @Override
-                       public Object getItem(int position) {
-                               return datas.get(position);
-                       }
-
-                       @Override
-                       public View getView(int position, View convertView, ViewGroup parent) {
-                               AndroidReaderBook book = books.get(convertView);
-                               if (book == null) {
-                                       book = new AndroidReaderBook();
-
-                                       FragmentTransaction trans = getFragmentManager()
-                                                       .beginTransaction();
-                                       trans.add(book, null);
-                                       trans.commit();
-                                       getFragmentManager().executePendingTransactions();
-
-                                       books.put(book.getView(), book);
-                               }
-
-                               MetaData meta = (MetaData) getItem(position);
-                               book.fill(meta, reader);
-
-                               return book.getView();
-                       }
-               });
-       }
-}
index 9ec37a583d983cee46333f32f793ea1dc04dc3b1..2a085a76e0b0829762c1b63794dc6a303d590143 100644 (file)
@@ -10,6 +10,10 @@ import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Paragraph;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.reader.BasicReader;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+import be.nikiroo.fanfix.searchable.SearchableTag;
+import be.nikiroo.fanfix.supported.SupportType;
+import be.nikiroo.utils.StringUtils;
 
 /**
  * Command line {@link Story} reader.
@@ -81,9 +85,8 @@ class CliReader extends BasicReader {
        }
 
        @Override
-       public void browse(String source) {
-               List<MetaData> stories;
-               stories = getLibrary().getListBySource(source);
+       public void browse(String source) throws IOException {
+               List<MetaData> stories = getLibrary().getListBySource(source);
 
                for (MetaData story : stories) {
                        String author = "";
@@ -95,4 +98,153 @@ class CliReader extends BasicReader {
                                        + author);
                }
        }
+
+       @Override
+       public void search(boolean sync) throws IOException {
+               for (SupportType type : SupportType.values()) {
+                       if (BasicSearchable.getSearchable(type) != null) {
+                               System.out.println(type);
+                       }
+               }
+       }
+
+       @Override
+       public void search(SupportType searchOn, String keywords, int page,
+                       int item, boolean sync) throws IOException {
+               BasicSearchable search = BasicSearchable.getSearchable(searchOn);
+
+               if (page == 0) {
+                       System.out.println(search.searchPages(keywords));
+               } else {
+                       List<MetaData> metas = search.search(keywords, page);
+
+                       if (item == 0) {
+                               System.out.println("Page " + page + " of stories for: "
+                                               + keywords);
+                               displayStories(metas);
+                       } else {
+                               // ! 1-based index !
+                               if (item <= 0 || item > metas.size()) {
+                                       throw new IOException("Index out of bounds: " + item);
+                               }
+
+                               MetaData meta = metas.get(item - 1);
+                               displayStory(meta);
+                       }
+               }
+       }
+
+       @Override
+       public void searchTag(SupportType searchOn, int page, int item,
+                       boolean sync, Integer... tags) throws IOException {
+
+               BasicSearchable search = BasicSearchable.getSearchable(searchOn);
+               SearchableTag stag = search.getTag(tags);
+
+               if (stag == null) {
+                       // TODO i18n
+                       System.out.println("Known tags: ");
+                       int i = 1;
+                       for (SearchableTag s : search.getTags()) {
+                               System.out.println(String.format("%d: %s", i, s.getName()));
+                               i++;
+                       }
+               } else {
+                       if (page <= 0) {
+                               if (stag.isLeaf()) {
+                                       System.out.println(search.searchPages(stag));
+                               } else {
+                                       System.out.println(stag.getCount());
+                               }
+                       } else {
+                               List<MetaData> metas = null;
+                               List<SearchableTag> subtags = null;
+                               int count;
+
+                               if (stag.isLeaf()) {
+                                       metas = search.search(stag, page);
+                                       count = metas.size();
+                               } else {
+                                       subtags = stag.getChildren();
+                                       count = subtags.size();
+                               }
+
+                               if (item > 0) {
+                                       if (item <= count) {
+                                               if (metas != null) {
+                                                       MetaData meta = metas.get(item - 1);
+                                                       displayStory(meta);
+                                               } else {
+                                                       SearchableTag subtag = subtags.get(item - 1);
+                                                       displayTag(subtag);
+                                               }
+                                       } else {
+                                               System.out.println("Invalid item: only " + count
+                                                               + " items found");
+                                       }
+                               } else {
+                                       if (metas != null) {
+                                               // TODO i18n
+                                               System.out.println(String.format("Content of %s: ",
+                                                               stag.getFqName()));
+                                               displayStories(metas);
+                                       } else {
+                                               // TODO i18n
+                                               System.out.println(String.format("Subtags of %s: ",
+                                                               stag.getFqName()));
+                                               displayTags(subtags);
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private void displayTag(SearchableTag subtag) {
+               // TODO: i18n
+               String stories = "stories";
+               String num = StringUtils.formatNumber(subtag.getCount());
+               System.out.println(String.format("%s (%s), %s %s", subtag.getName(),
+                               subtag.getFqName(), num, stories));
+       }
+
+       private void displayStory(MetaData meta) {
+               System.out.println(meta.getTitle());
+               System.out.println();
+               System.out.println(meta.getUrl());
+               System.out.println();
+               System.out.println("Tags: " + meta.getTags());
+               System.out.println();
+               for (Paragraph para : meta.getResume()) {
+                       System.out.println(para.getContent());
+                       System.out.println("");
+               }
+       }
+
+       private void displayTags(List<SearchableTag> subtags) {
+               int i = 1;
+               for (SearchableTag subtag : subtags) {
+                       String total = "";
+                       if (subtag.getCount() > 0) {
+                               total = StringUtils.formatNumber(subtag.getCount());
+                       }
+
+                       if (total.isEmpty()) {
+                               System.out
+                                               .println(String.format("%d: %s", i, subtag.getName()));
+                       } else {
+                               System.out.println(String.format("%d: %s (%s)", i,
+                                               subtag.getName(), total));
+                       }
+
+                       i++;
+               }
+       }
+
+       private void displayStories(List<MetaData> metas) {
+               int i = 1;
+               for (MetaData meta : metas) {
+                       System.out.println(i + ": " + meta.getTitle());
+                       i++;
+               }
+       }
 }
index f94f7838c2552e41d7061cd59e3c5bb3328ca7a9..bef84eae46c92ae9f41ea14f0c69f75d99e5927d 100644 (file)
@@ -7,6 +7,7 @@ import jexer.TApplication.BackendType;
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.reader.Reader;
+import be.nikiroo.fanfix.supported.SupportType;
 
 /**
  * This {@link Reader}is based upon the TUI widget library 'jexer'
@@ -70,4 +71,30 @@ class TuiReader extends BasicReader {
                        Instance.getTraceHandler().error(e);
                }
        }
+
+       @Override
+       public void search(boolean sync) throws IOException {
+               // TODO
+               if (sync) {
+                       throw new java.lang.IllegalStateException("Not implemented yet.");
+               }
+       }
+
+       @Override
+       public void search(SupportType searchOn, String keywords, int page,
+                       int item, boolean sync) {
+               // TODO
+               if (sync) {
+                       throw new java.lang.IllegalStateException("Not implemented yet.");
+               }
+       }
+
+       @Override
+       public void searchTag(SupportType searchOn, int page, int item,
+                       boolean sync, Integer... tags) {
+               // TODO
+               if (sync) {
+                       throw new java.lang.IllegalStateException("Not implemented yet.");
+               }
+       }
 }
index f08b84ccbbf17328283b059fae0ec960fe29cee1..b6f31ff4e9b43b98e4fadf3bc655b66f02225fa4 100644 (file)
@@ -25,6 +25,7 @@ import be.nikiroo.fanfix.library.BasicLibrary;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.reader.Reader;
 import be.nikiroo.fanfix.reader.tui.TuiReaderMainWindow.Mode;
+import be.nikiroo.fanfix.supported.SupportType;
 import be.nikiroo.utils.Progress;
 
 /**
@@ -81,7 +82,7 @@ class TuiReaderApplication extends TApplication implements Reader {
        }
 
        @Override
-       public Story getStory(Progress pg) {
+       public Story getStory(Progress pg) throws IOException {
                return reader.getStory(pg);
        }
 
@@ -112,7 +113,11 @@ class TuiReaderApplication extends TApplication implements Reader {
 
        @Override
        public void browse(String source) {
-               reader.browse(source);
+               try {
+                       reader.browse(source);
+               } catch (IOException e) {
+                       Instance.getTraceHandler().error(e);
+               }
        }
 
        @Override
@@ -125,6 +130,23 @@ class TuiReaderApplication extends TApplication implements Reader {
                reader.setChapter(chapter);
        }
 
+       @Override
+       public void search(boolean sync) throws IOException {
+               reader.search(sync);
+       }
+
+       @Override
+       public void search(SupportType searchOn, String keywords, int page,
+                       int item, boolean sync) throws IOException {
+               reader.search(searchOn, keywords, page, item, sync);
+       }
+
+       @Override
+       public void searchTag(SupportType searchOn, int page, int item,
+                       boolean sync, Integer... tags) throws IOException {
+               reader.searchTag(searchOn, page, item, sync, tags);
+       }
+
        /**
         * Open the given {@link Story} for reading. This may or may not start an
         * external program to read said {@link Story}.
index 932cbcbb03fb33a5bf1a3721282c1866a41e801e..426355fa2eb40d86e130a07e36a7f3763e5ee62a 100644 (file)
@@ -159,15 +159,24 @@ class TuiReaderMainWindow extends TWindow {
                                } else if (smode.equals("Sources")) {
                                        selectTargets.clear();
                                        selectTargets.add("(show all)");
-                                       for (String source : reader.getLibrary().getSources()) {
-                                               selectTargets.add(source);
+                                       try {
+                                               for (String source : reader.getLibrary().getSources()) {
+                                                       selectTargets.add(source);
+                                               }
+                                       } catch (IOException e) {
+                                               Instance.getTraceHandler().error(e);
                                        }
+
                                        showTarget = true;
                                } else {
                                        selectTargets.clear();
                                        selectTargets.add("(show all)");
-                                       for (String author : reader.getLibrary().getAuthors()) {
-                                               selectTargets.add(author);
+                                       try {
+                                               for (String author : reader.getLibrary().getAuthors()) {
+                                                       selectTargets.add(author);
+                                               }
+                                       } catch (IOException e) {
+                                               Instance.getTraceHandler().error(e);
                                        }
 
                                        showTarget = true;
@@ -231,12 +240,18 @@ class TuiReaderMainWindow extends TWindow {
         */
        public void refreshStories() {
                List<MetaData> metas;
-               if (mode == Mode.SOURCE) {
-                       metas = reader.getLibrary().getListBySource(target);
-               } else if (mode == Mode.AUTHOR) {
-                       metas = reader.getLibrary().getListByAuthor(target);
-               } else {
-                       metas = reader.getLibrary().getList();
+
+               try {
+                       if (mode == Mode.SOURCE) {
+                               metas = reader.getLibrary().getListBySource(target);
+                       } else if (mode == Mode.AUTHOR) {
+                               metas = reader.getLibrary().getListByAuthor(target);
+                       } else {
+                               metas = reader.getLibrary().getList();
+                       }
+               } catch (IOException e) {
+                       Instance.getTraceHandler().error(e);
+                       metas = new ArrayList<MetaData>();
                }
 
                setMetas(metas);
index f4a932b3b899ef3fc48826bbe0b3aaa913a68d90..b720af409250248312d3d0cc5a2a2da45c158ddb 100644 (file)
@@ -25,6 +25,9 @@ import be.nikiroo.fanfix.library.BasicLibrary;
 import be.nikiroo.fanfix.library.CacheLibrary;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.reader.Reader;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+import be.nikiroo.fanfix.searchable.SearchableTag;
+import be.nikiroo.fanfix.supported.SupportType;
 import be.nikiroo.utils.Progress;
 import be.nikiroo.utils.Version;
 import be.nikiroo.utils.ui.UIUtils;
@@ -218,6 +221,64 @@ class GuiReader extends BasicReader {
                }
        }
 
+       @Override
+       public void search(boolean sync) throws IOException {
+               GuiReaderSearchFrame search = new GuiReaderSearchFrame(this);
+               if (sync) {
+                       sync(search);
+               } else {
+                       search.setVisible(true);
+               }
+       }
+
+       @Override
+       public void search(SupportType searchOn, String keywords, int page,
+                       int item, boolean sync) {
+               GuiReaderSearchFrame search = new GuiReaderSearchFrame(this);
+               search.search(searchOn, keywords, page, item);
+               if (sync) {
+                       sync(search);
+               } else {
+                       search.setVisible(true);
+               }
+       }
+
+       @Override
+       public void searchTag(final SupportType searchOn, final int page,
+                       final int item, final boolean sync, final Integer... tags) {
+
+               final GuiReaderSearchFrame search = new GuiReaderSearchFrame(this);
+
+               final BasicSearchable searchable = BasicSearchable
+                               .getSearchable(searchOn);
+
+               Runnable action = new Runnable() {
+                       @Override
+                       public void run() {
+                               SearchableTag tag = null;
+                               try {
+                                       tag = searchable.getTag(tags);
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
+                               }
+
+                               search.searchTag(searchOn, page, item, tag);
+
+                               if (sync) {
+                                       sync(search);
+                               } else {
+                                       search.setVisible(true);
+                               }
+                       }
+               };
+
+               if (sync) {
+                       action.run();
+               } else {
+                       new Thread(action).start();
+               }
+       }
+
        /**
         * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
         * from the main library.
index 23d4c3160169e0244a1070720d376fc0aba06ab6..f071be02e59d559991317a57c193ab8e04e2fb81 100644 (file)
@@ -1,10 +1,13 @@
 package be.nikiroo.fanfix.reader.ui;
 
+import java.io.IOException;
+
 import be.nikiroo.fanfix.bundles.StringIdGui;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.BasicLibrary;
 import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
 
 /**
  * Some meta information related to a "book" (which can either be a
@@ -122,11 +125,22 @@ public class GuiReaderBookInfo {
         *            the {@link BasicLibrary} to use to fetch the image
         * 
         * @return the base image
+        * 
+        * @throws IOException
+        *             in case of I/O error
         */
-       public Image getBaseImage(BasicLibrary lib) {
+       public Image getBaseImage(BasicLibrary lib) throws IOException {
                switch (type) {
                case STORY:
-                       return lib.getCover(meta.getLuid());
+                       if (meta.getCover() != null) {
+                               return meta.getCover();
+                       }
+
+                       if (meta.getLuid() != null) {
+                               return lib.getCover(meta.getLuid());
+                       }
+
+                       return null;
                case SOURCE:
                        return lib.getSourceCover(value);
                case AUTHOR:
@@ -149,12 +163,15 @@ public class GuiReaderBookInfo {
                if (uid == null || uid.trim().isEmpty()) {
                        uid = meta.getLuid();
                }
+               if (uid == null || uid.trim().isEmpty()) {
+                       uid = meta.getUrl();
+               }
 
                GuiReaderBookInfo info = new GuiReaderBookInfo(Type.STORY, uid,
                                meta.getTitle());
 
                info.meta = meta;
-               info.count = formatNumber(meta.getWords());
+               info.count = StringUtils.formatNumber(meta.getWords());
                if (!info.count.isEmpty()) {
                        info.count = GuiReader.trans(
                                        meta.isImageDocument() ? StringIdGui.BOOK_COUNT_IMAGES
@@ -179,7 +196,13 @@ public class GuiReaderBookInfo {
                GuiReaderBookInfo info = new GuiReaderBookInfo(Type.SOURCE, "source_"
                                + source, source);
 
-               info.count = formatNumber(lib.getListBySource(source).size());
+               int size = 0;
+               try {
+                       size = lib.getListBySource(source).size();
+               } catch (IOException e) {
+               }
+
+               info.count = StringUtils.formatNumber(size);
                if (!info.count.isEmpty()) {
                        info.count = GuiReader.trans(StringIdGui.BOOK_COUNT_STORIES,
                                        info.count);
@@ -203,7 +226,13 @@ public class GuiReaderBookInfo {
                GuiReaderBookInfo info = new GuiReaderBookInfo(Type.AUTHOR, "author_"
                                + author, author);
 
-               info.count = formatNumber(lib.getListByAuthor(author).size());
+               int size = 0;
+               try {
+                       size = lib.getListByAuthor(author).size();
+               } catch (IOException e) {
+               }
+
+               info.count = StringUtils.formatNumber(size);
                if (!info.count.isEmpty()) {
                        info.count = GuiReader.trans(StringIdGui.BOOK_COUNT_STORIES,
                                        info.count);
@@ -211,26 +240,4 @@ public class GuiReaderBookInfo {
 
                return info;
        }
-
-       /**
-        * Format a number for display (use the "k" notation if higher or equal to
-        * 4000).
-        * 
-        * @param number
-        *            the number to parse
-        * 
-        * @return the displayable version of the number
-        */
-       static private String formatNumber(long number) {
-               String displayNumber;
-               if (number >= 4000) {
-                       displayNumber = "" + (number / 1000) + "k";
-               } else if (number > 0) {
-                       displayNumber = "" + number;
-               } else {
-                       displayNumber = "";
-               }
-
-               return displayNumber;
-       }
 }
index 0bbf82eb47c29dcaab4a4a3fdee38cf9a995e7c3..f46ec1bb8c2d9279b58666d015b4ab9e7c7ad48b 100644 (file)
@@ -129,6 +129,24 @@ class GuiReaderCoverImager {
                return generateCoverIcon(lib, GuiReaderBookInfo.fromMeta(meta));
        }
 
+       /**
+        * The width of a cover image.
+        * 
+        * @return the width
+        */
+       static public int getCoverWidth() {
+               return SPINE_WIDTH + COVER_WIDTH;
+       }
+
+       /**
+        * The height of a cover image.
+        * 
+        * @return the height
+        */
+       static public int getCoverHeight() {
+               return COVER_HEIGHT + HOFFSET;
+       }
+
        /**
         * Generate a cover icon based upon the given {@link GuiReaderBookInfo}.
         * 
@@ -158,9 +176,8 @@ class GuiReaderCoverImager {
                if (resizedImage == null) {
                        try {
                                Image cover = info.getBaseImage(lib);
-                               resizedImage = new BufferedImage(SPINE_WIDTH + COVER_WIDTH,
-                                               SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
-                                               BufferedImage.TYPE_4BYTE_ABGR);
+                               resizedImage = new BufferedImage(getCoverWidth(),
+                                               getCoverHeight(), BufferedImage.TYPE_4BYTE_ABGR);
 
                                Graphics2D g = resizedImage.createGraphics();
                                try {
index e207023ddf2b145bc4c4e0e69ea5836d8abfff1f..a28dc8a0d275bda522b46f2a96d860907ba40f37 100644 (file)
@@ -31,11 +31,14 @@ import be.nikiroo.fanfix.bundles.UiConfig;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.BasicLibrary;
+import be.nikiroo.fanfix.library.BasicLibrary.Status;
 import be.nikiroo.fanfix.library.LocalLibrary;
 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.reader.ui.GuiReaderMainPanel.FrameHelper;
 import be.nikiroo.fanfix.reader.ui.GuiReaderMainPanel.StoryRunnable;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+import be.nikiroo.fanfix.supported.SupportType;
 import be.nikiroo.utils.Progress;
 import be.nikiroo.utils.Version;
 import be.nikiroo.utils.ui.ConfigEditor;
@@ -78,7 +81,7 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
         */
        public GuiReaderFrame(GuiReader reader, String type) {
                super(getAppTitle(reader.getLibrary().getLibraryName()));
-               
+
                this.reader = reader;
 
                mainPanel = new GuiReaderMainPanel(this, type);
@@ -90,20 +93,25 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
 
        @Override
        public JPopupMenu createBookPopup() {
+               Status status = reader.getLibrary().getStatus();
                JPopupMenu popup = new JPopupMenu();
                popup.add(createMenuItemOpenBook());
                popup.addSeparator();
                popup.add(createMenuItemExport());
-               popup.add(createMenuItemMoveTo(true));
-               popup.add(createMenuItemSetCoverForSource());
-               popup.add(createMenuItemSetCoverForAuthor());
+               if (status.isWritable()) {
+                       popup.add(createMenuItemMoveTo());
+                       popup.add(createMenuItemSetCoverForSource());
+                       popup.add(createMenuItemSetCoverForAuthor());
+               }
                popup.add(createMenuItemClearCache());
-               popup.add(createMenuItemRedownload());
-               popup.addSeparator();
-               popup.add(createMenuItemRename(true));
-               popup.add(createMenuItemSetAuthor(true));
-               popup.addSeparator();
-               popup.add(createMenuItemDelete());
+               if (status.isWritable()) {
+                       popup.add(createMenuItemRedownload());
+                       popup.addSeparator();
+                       popup.add(createMenuItemRename());
+                       popup.add(createMenuItemSetAuthor());
+                       popup.addSeparator();
+                       popup.add(createMenuItemDelete());
+               }
                popup.addSeparator();
                popup.add(createMenuItemProperties());
                return popup;
@@ -117,7 +125,7 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
        }
 
        @Override
-       public void createMenu(boolean libOk) {
+       public void createMenu(Status status) {
                invalidate();
 
                JMenuBar bar = new JMenuBar();
@@ -155,13 +163,15 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
 
                file.add(createMenuItemOpenBook());
                file.add(createMenuItemExport());
-               file.add(createMenuItemMoveTo(libOk));
-               file.addSeparator();
-               file.add(imprt);
-               file.add(imprtF);
-               file.addSeparator();
-               file.add(createMenuItemRename(libOk));
-               file.add(createMenuItemSetAuthor(libOk));
+               if (status.isWritable()) {
+                       file.add(createMenuItemMoveTo());
+                       file.addSeparator();
+                       file.add(imprt);
+                       file.add(imprtF);
+                       file.addSeparator();
+                       file.add(createMenuItemRename());
+                       file.add(createMenuItemSetAuthor());
+               }
                file.addSeparator();
                file.add(createMenuItemProperties());
                file.addSeparator();
@@ -181,6 +191,24 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
 
                bar.add(edit);
 
+               JMenu search = new JMenu(GuiReader.trans(StringIdGui.MENU_SEARCH));
+               search.setMnemonic(KeyEvent.VK_H);
+               for (final SupportType type : SupportType.values()) {
+                       BasicSearchable searchable = BasicSearchable.getSearchable(type);
+                       if (searchable != null) {
+                               JMenuItem searchItem = new JMenuItem(type.getSourceName());
+                               searchItem.addActionListener(new ActionListener() {
+                                       @Override
+                                       public void actionPerformed(ActionEvent e) {
+                                               reader.search(type, null, 1, 0, false);
+                                       }
+                               });
+                               search.add(searchItem);
+                       }
+               }
+
+               bar.add(search);
+
                JMenu view = new JMenu(GuiReader.trans(StringIdGui.MENU_VIEW));
                view.setMnemonic(KeyEvent.VK_V);
                JMenuItem vauthors = new JMenuItem(
@@ -208,8 +236,12 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                bar.add(view);
 
                Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
-               if (libOk) {
-                       groupedSources = reader.getLibrary().getSourcesGrouped();
+               if (status.isReady()) {
+                       try {
+                               groupedSources = reader.getLibrary().getSourcesGrouped();
+                       } catch (IOException e) {
+                               error(e.getLocalizedMessage(), "IOException", e);
+                       }
                }
                JMenu sources = new JMenu(GuiReader.trans(StringIdGui.MENU_SOURCES));
                sources.setMnemonic(KeyEvent.VK_S);
@@ -217,8 +249,12 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                bar.add(sources);
 
                Map<String, List<String>> goupedAuthors = new HashMap<String, List<String>>();
-               if (libOk) {
-                       goupedAuthors = reader.getLibrary().getAuthorsGrouped();
+               if (status.isReady()) {
+                       try {
+                               goupedAuthors = reader.getLibrary().getAuthorsGrouped();
+                       } catch (IOException e) {
+                               error(e.getLocalizedMessage(), "IOException", e);
+                       }
                }
                JMenu authors = new JMenu(GuiReader.trans(StringIdGui.MENU_AUTHORS));
                authors.setMnemonic(KeyEvent.VK_A);
@@ -324,9 +360,13 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                        final boolean listMode) {
                return new ActionListener() {
                        @Override
-                       public void actionPerformed(ActionEvent e) {
+                       public void actionPerformed(ActionEvent ae) {
                                mainPanel.removeBookPanes();
-                               mainPanel.addBookPane(type, listMode);
+                               try {
+                                       mainPanel.addBookPane(type, listMode);
+                               } catch (IOException e) {
+                                       error(e.getLocalizedMessage(), "IOException", e);
+                               }
                                mainPanel.refreshBooks();
                        }
                };
@@ -421,7 +461,8 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                        fc.showDialog(GuiReaderFrame.this,
                                                        GuiReader.trans(StringIdGui.TITLE_SAVE));
                                        if (fc.getSelectedFile() != null) {
-                                               final OutputType type = otherFilters.get(fc.getFileFilter());
+                                               final OutputType type = otherFilters.get(fc
+                                                               .getFileFilter());
                                                final String path = fc.getSelectedFile()
                                                                .getAbsolutePath()
                                                                + type.getDefaultExtension(false);
@@ -509,19 +550,18 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
        /**
         * Create the "move to" menu item.
         * 
-        * @param libOk
-        *            the library can be queried
-        * 
         * @return the item
         */
-       private JMenuItem createMenuItemMoveTo(boolean libOk) {
+       private JMenuItem createMenuItemMoveTo() {
                JMenu changeTo = new JMenu(
                                GuiReader.trans(StringIdGui.MENU_FILE_MOVE_TO));
                changeTo.setMnemonic(KeyEvent.VK_M);
 
                Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
-               if (libOk) {
+               try {
                        groupedSources = reader.getLibrary().getSourcesGrouped();
+               } catch (IOException e) {
+                       error(e.getLocalizedMessage(), "IOException", e);
                }
 
                JMenuItem item = new JMenuItem(
@@ -562,12 +602,9 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
        /**
         * Create the "set author" menu item.
         * 
-        * @param libOk
-        *            the library can be queried
-        * 
         * @return the item
         */
-       private JMenuItem createMenuItemSetAuthor(boolean libOk) {
+       private JMenuItem createMenuItemSetAuthor() {
                JMenu changeTo = new JMenu(
                                GuiReader.trans(StringIdGui.MENU_FILE_SET_AUTHOR));
                changeTo.setMnemonic(KeyEvent.VK_A);
@@ -580,34 +617,39 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                newItem.addActionListener(createMoveAction(ChangeAction.AUTHOR, null));
 
                // Existing authors
-               if (libOk) {
-                       Map<String, List<String>> groupedAuthors = reader.getLibrary()
-                                       .getAuthorsGrouped();
-
-                       if (groupedAuthors.size() > 1) {
-                               for (String key : groupedAuthors.keySet()) {
-                                       JMenu group = new JMenu(key);
-                                       for (String value : groupedAuthors.get(key)) {
-                                               JMenuItem item = new JMenuItem(
-                                                               value.isEmpty() ? GuiReader
-                                                                               .trans(StringIdGui.MENU_AUTHORS_UNKNOWN)
-                                                                               : value);
-                                               item.addActionListener(createMoveAction(
-                                                               ChangeAction.AUTHOR, value));
-                                               group.add(item);
-                                       }
-                                       changeTo.add(group);
-                               }
-                       } else if (groupedAuthors.size() == 1) {
-                               for (String value : groupedAuthors.values().iterator().next()) {
+               Map<String, List<String>> groupedAuthors;
+
+               try {
+                       groupedAuthors = reader.getLibrary().getAuthorsGrouped();
+               } catch (IOException e) {
+                       error(e.getLocalizedMessage(), "IOException", e);
+                       groupedAuthors = new HashMap<String, List<String>>();
+
+               }
+
+               if (groupedAuthors.size() > 1) {
+                       for (String key : groupedAuthors.keySet()) {
+                               JMenu group = new JMenu(key);
+                               for (String value : groupedAuthors.get(key)) {
                                        JMenuItem item = new JMenuItem(
                                                        value.isEmpty() ? GuiReader
                                                                        .trans(StringIdGui.MENU_AUTHORS_UNKNOWN)
                                                                        : value);
                                        item.addActionListener(createMoveAction(
                                                        ChangeAction.AUTHOR, value));
-                                       changeTo.add(item);
+                                       group.add(item);
                                }
+                               changeTo.add(group);
+                       }
+               } else if (groupedAuthors.size() == 1) {
+                       for (String value : groupedAuthors.values().iterator().next()) {
+                               JMenuItem item = new JMenuItem(
+                                               value.isEmpty() ? GuiReader
+                                                               .trans(StringIdGui.MENU_AUTHORS_UNKNOWN)
+                                                               : value);
+                               item.addActionListener(createMoveAction(ChangeAction.AUTHOR,
+                                               value));
+                               changeTo.add(item);
                        }
                }
 
@@ -617,13 +659,9 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
        /**
         * Create the "rename" menu item.
         * 
-        * @param libOk
-        *            the library can be queried
-        * 
         * @return the item
         */
-       private JMenuItem createMenuItemRename(
-                       @SuppressWarnings("unused") boolean libOk) {
+       private JMenuItem createMenuItemRename() {
                JMenuItem changeTo = new JMenuItem(
                                GuiReader.trans(StringIdGui.MENU_FILE_RENAME));
                changeTo.setMnemonic(KeyEvent.VK_R);
@@ -693,7 +731,7 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                                        SwingUtilities.invokeLater(new Runnable() {
                                                                @Override
                                                                public void run() {
-                                                                       createMenu(true);
+                                                                       createMenu(reader.getLibrary().getStatus());
                                                                }
                                                        });
                                                }
@@ -718,21 +756,16 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
                                        final MetaData meta = selectedBook.getInfo().getMeta();
-                                       mainPanel.imprt(
-                                                       meta.getUrl(),
-                                                       new StoryRunnable() {
-                                                               @Override
-                                                               public void run(Story story) {
-                                                                       MetaData newMeta = story.getMeta();
-                                                                       if (!newMeta.getSource().equals(
-                                                                                       meta.getSource())) {
-                                                                               reader.changeSource(newMeta.getLuid(),
-                                                                                               meta.getSource());
-                                                                       }
-                                                               }
-                                                       },
-                                                       GuiReader
-                                                                       .trans(StringIdGui.PROGRESS_CHANGE_SOURCE));
+                                       mainPanel.imprt(meta.getUrl(), new StoryRunnable() {
+                                               @Override
+                                               public void run(Story story) {
+                                                       MetaData newMeta = story.getMeta();
+                                                       if (!newMeta.getSource().equals(meta.getSource())) {
+                                                               reader.changeSource(newMeta.getLuid(),
+                                                                               meta.getSource());
+                                                       }
+                                               }
+                                       }, GuiReader.trans(StringIdGui.PROGRESS_CHANGE_SOURCE));
                                }
                        }
                });
@@ -848,7 +881,7 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                KeyEvent.VK_C);
                open.addActionListener(new ActionListener() {
                        @Override
-                       public void actionPerformed(ActionEvent e) {
+                       public void actionPerformed(ActionEvent ae) {
                                final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
                                        BasicLibrary lib = reader.getLibrary();
@@ -856,7 +889,11 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                        String source = selectedBook.getInfo().getMeta()
                                                        .getSource();
 
-                                       lib.setSourceCover(source, luid);
+                                       try {
+                                               lib.setSourceCover(source, luid);
+                                       } catch (IOException e) {
+                                               error(e.getLocalizedMessage(), "IOException", e);
+                                       }
 
                                        GuiReaderBookInfo sourceInfo = GuiReaderBookInfo
                                                        .fromSource(lib, source);
@@ -880,7 +917,7 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                KeyEvent.VK_A);
                open.addActionListener(new ActionListener() {
                        @Override
-                       public void actionPerformed(ActionEvent e) {
+                       public void actionPerformed(ActionEvent ae) {
                                final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
                                        BasicLibrary lib = reader.getLibrary();
@@ -888,7 +925,11 @@ class GuiReaderFrame extends JFrame implements FrameHelper {
                                        String author = selectedBook.getInfo().getMeta()
                                                        .getAuthor();
 
-                                       lib.setAuthorCover(author, luid);
+                                       try {
+                                               lib.setAuthorCover(author, luid);
+                                       } catch (IOException e) {
+                                               error(e.getLocalizedMessage(), "IOException", e);
+                                       }
 
                                        GuiReaderBookInfo authorInfo = GuiReaderBookInfo
                                                        .fromAuthor(lib, author);
index ffbcda37b19fbc19d62d44d43a94900fd205b001..cc3f1e15f59794e4c3117b3f9efe3e9d1be90ecd 100644 (file)
@@ -3,6 +3,8 @@ package be.nikiroo.fanfix.reader.ui;
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Rectangle;
 import java.awt.event.ActionListener;
 import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
@@ -29,10 +31,13 @@ public class GuiReaderGroup extends JPanel {
        private static final long serialVersionUID = 1L;
        private BookActionListener action;
        private Color backgroundColor;
+       private Color backgroundColorDef;
+       private Color backgroundColorDefPane;
        private GuiReader reader;
        private List<GuiReaderBookInfo> infos;
        private List<GuiReaderBook> books;
        private JPanel pane;
+       private JLabel titleLabel;
        private boolean words; // words or authors (secondary info on books)
        private int itemsPerLine;
 
@@ -43,21 +48,20 @@ public class GuiReaderGroup extends JPanel {
         *            the {@link GuiReaderBook} used to probe some information about
         *            the stories
         * @param title
-        *            the title of this group
+        *            the title of this group (can be NULL for "no title", an empty
+        *            {@link String} will trigger a default title for empty groups)
         * @param backgroundColor
         *            the background colour to use (or NULL for default)
         */
        public GuiReaderGroup(GuiReader reader, String title, Color backgroundColor) {
                this.reader = reader;
-               this.backgroundColor = backgroundColor;
 
                this.pane = new JPanel();
-
                pane.setLayout(new WrapLayout(WrapLayout.LEADING, 5, 5));
-               if (backgroundColor != null) {
-                       pane.setBackground(backgroundColor);
-                       setBackground(backgroundColor);
-               }
+
+               this.backgroundColorDef = getBackground();
+               this.backgroundColorDefPane = pane.getBackground();
+               setBackground(backgroundColor);
 
                setLayout(new BorderLayout(0, 10));
 
@@ -68,18 +72,10 @@ public class GuiReaderGroup extends JPanel {
 
                add(pane, BorderLayout.CENTER);
 
-               if (title != null) {
-                       if (title.isEmpty()) {
-                               title = GuiReader.trans(StringIdGui.MENU_AUTHORS_UNKNOWN);
-                       }
-
-                       JLabel label = new JLabel();
-                       label.setText(String.format("<html>"
-                                       + "<body style='text-align: center; color: gray;'><br><b>"
-                                       + "%s" + "</b></body>" + "</html>", title));
-                       label.setHorizontalAlignment(JLabel.CENTER);
-                       add(label, BorderLayout.NORTH);
-               }
+               titleLabel = new JLabel();
+               titleLabel.setHorizontalAlignment(JLabel.CENTER);
+               add(titleLabel, BorderLayout.NORTH);
+               setTitle(title);
 
                // Compute the number of items per line at each resize
                addComponentListener(new ComponentAdapter() {
@@ -119,13 +115,79 @@ public class GuiReaderGroup extends JPanel {
                });
        }
 
+       /**
+        * Note: this class supports NULL as a background colour, which will revert
+        * it to its default state.
+        * <p>
+        * Note: this class' implementation will also set the main pane background
+        * colour at the same time.
+        * <p>
+        * Sets the background colour of this component. The background colour is
+        * used only if the component is opaque, and only by subclasses of
+        * <code>JComponent</code> or <code>ComponentUI</code> implementations.
+        * Direct subclasses of <code>JComponent</code> must override
+        * <code>paintComponent</code> to honour this property.
+        * <p>
+        * It is up to the look and feel to honour this property, some may choose to
+        * ignore it.
+        * 
+        * @param backgroundColor
+        *            the desired background <code>Colour</code>
+        * @see java.awt.Component#getBackground
+        * @see #setOpaque
+        * 
+        * @beaninfo preferred: true bound: true attribute: visualUpdate true
+        *           description: The background colour of the component.
+        */
+       @Override
+       public void setBackground(Color backgroundColor) {
+               this.backgroundColor = backgroundColor;
+               
+               Color cme = backgroundColor == null ? backgroundColorDef
+                               : backgroundColor;
+               Color cpane = backgroundColor == null ? backgroundColorDefPane
+                               : backgroundColor;
+
+               if (pane != null) { // can happen at theme setup time
+                       pane.setBackground(cpane);
+               }
+               super.setBackground(cme);
+       }
+
+       /**
+        * The title of this group (can be NULL for "no title", an empty
+        * {@link String} will trigger a default title for empty groups)
+        * 
+        * @param title
+        *            the title or NULL
+        */
+       public void setTitle(String title) {
+               if (title != null) {
+                       if (title.isEmpty()) {
+                               title = GuiReader.trans(StringIdGui.MENU_AUTHORS_UNKNOWN);
+                       }
+
+                       titleLabel.setText(String.format("<html>"
+                                       + "<body style='text-align: center; color: gray;'><br><b>"
+                                       + "%s" + "</b></body>" + "</html>", title));
+                       titleLabel.setVisible(true);
+               } else {
+                       titleLabel.setVisible(false);
+               }
+       }
+
        /**
         * Compute how many items can fit in a line so UP and DOWN can be used to go
         * up/down one line at a time.
         */
        private void computeItemsPerLine() {
-               // TODO
-               itemsPerLine = 5;
+               itemsPerLine = 1;
+
+               if (books != null && books.size() > 0) {
+                       // this.pane holds all the books with a hgap of 5 px
+                       int wbook = books.get(0).getWidth() + 5;
+                       itemsPerLine = pane.getWidth() / wbook;
+               }
        }
 
        /**
@@ -137,6 +199,30 @@ public class GuiReaderGroup extends JPanel {
         */
        public void setActionListener(BookActionListener action) {
                this.action = action;
+               refreshBooks();
+       }
+
+       /**
+        * Clear all the books in this {@link GuiReaderGroup}.
+        */
+       public void clear() {
+               refreshBooks(new ArrayList<GuiReaderBookInfo>());
+       }
+
+       /**
+        * Refresh the list of {@link GuiReaderBook}s displayed in the control.
+        */
+       public void refreshBooks() {
+               refreshBooks(infos, words);
+       }
+
+       /**
+        * Refresh the list of {@link GuiReaderBook}s displayed in the control.
+        * 
+        * @param infos
+        *            the new list of infos
+        */
+       public void refreshBooks(List<GuiReaderBookInfo> infos) {
                refreshBooks(infos, words);
        }
 
@@ -172,7 +258,7 @@ public class GuiReaderGroup extends JPanel {
                if (infos != null) {
                        for (GuiReaderBookInfo info : infos) {
                                boolean isCached = false;
-                               if (info.getMeta() != null) {
+                               if (info.getMeta() != null && info.getMeta().getLuid() != null) {
                                        isCached = reader.isCached(info.getMeta().getLuid());
                                }
 
@@ -215,6 +301,8 @@ public class GuiReaderGroup extends JPanel {
                pane.repaint();
                validate();
                repaint();
+
+               computeItemsPerLine();
        }
 
        /**
@@ -242,12 +330,21 @@ public class GuiReaderGroup extends JPanel {
                repaint();
        }
 
+       /**
+        * The number of books in this group.
+        * 
+        * @return the count
+        */
+       public int getBooksCount() {
+               return books.size();
+       }
+
        /**
         * Return the index of the currently selected book if any, -1 if none.
         * 
         * @return the index or -1
         */
-       private int getSelectedBookIndex() {
+       public int getSelectedBookIndex() {
                int index = -1;
                for (int i = 0; i < books.size(); i++) {
                        if (books.get(i).isSelected()) {
@@ -264,12 +361,12 @@ public class GuiReaderGroup extends JPanel {
         * @param index
         *            the index of the book to select, can be outside the bounds
         *            (either all the items will be unselected or the first or last
-        *            book will then be selected, see <tt>forceRange>/tt>)
+        *            book will then be selected, see <tt>forceRange></tt>)
         * @param forceRange
         *            TRUE to constraint the index to the first/last element, FALSE
         *            to unselect when outside the range
         */
-       private void setSelectedBook(int index, boolean forceRange) {
+       public void setSelectedBook(int index, boolean forceRange) {
                int previousIndex = getSelectedBookIndex();
 
                if (index >= books.size()) {
@@ -288,7 +385,7 @@ public class GuiReaderGroup extends JPanel {
                        books.get(previousIndex).setSelected(false);
                }
 
-               if (index >= 0) {
+               if (index >= 0 && !books.isEmpty()) {
                        books.get(index).setSelected(true);
                }
        }
@@ -361,4 +458,19 @@ public class GuiReaderGroup extends JPanel {
                        e.consume();
                }
        }
+
+       @Override
+       public void paint(Graphics g) {
+               super.paint(g);
+
+               Rectangle clip = g.getClipBounds();
+               if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
+                       return;
+               }
+
+               if (!isEnabled()) {
+                       g.setColor(new Color(128, 128, 128, 128));
+                       g.fillRect(clip.x, clip.y, clip.width, clip.height);
+               }
+       }
 }
index cfd1e947013cbff6916b96f54a096e426e957422..8593fe6471c816812220defeedf5775fe1757642 100644 (file)
@@ -85,10 +85,10 @@ class GuiReaderMainPanel extends JPanel {
                 * <p>
                 * Will invalidate the layout.
                 * 
-                * @param libOk
-                *            the library can be queried
+                * @param status
+                *            the library status, <b>must not</b> be NULL
                 */
-               public void createMenu(boolean libOk);
+               public void createMenu(Status status);
 
                /**
                 * Create a popup menu for a {@link GuiReaderBook} that represents a
@@ -138,6 +138,7 @@ class GuiReaderMainPanel extends JPanel {
 
                pane = new JPanel();
                pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
+               JScrollPane scroll = new JScrollPane(pane);
 
                Integer icolor = Instance.getUiConfig().getColor(
                                UiConfig.BACKGROUND_COLOR);
@@ -145,9 +146,9 @@ class GuiReaderMainPanel extends JPanel {
                        color = new Color(icolor);
                        setBackground(color);
                        pane.setBackground(color);
+                       scroll.setBackground(color);
                }
 
-               JScrollPane scroll = new JScrollPane(pane);
                scroll.getVerticalScrollBar().setUnitIncrement(16);
                add(scroll, BorderLayout.CENTER);
 
@@ -197,23 +198,28 @@ class GuiReaderMainPanel extends JPanel {
                                final BasicLibrary lib = helper.getReader().getLibrary();
                                final Status status = lib.getStatus();
 
-                               if (status == Status.READY) {
+                               if (status == Status.READ_WRITE) {
                                        lib.refresh(pg);
                                }
 
                                inUi(new Runnable() {
                                        @Override
                                        public void run() {
-                                               if (status == Status.READY) {
-                                                       helper.createMenu(true);
+                                               if (status.isReady()) {
+                                                       helper.createMenu(status);
                                                        pane.setVisible(true);
                                                        if (typeF == null) {
-                                                               addBookPane(true, false);
+                                                               try {
+                                                                       addBookPane(true, false);
+                                                               } catch (IOException e) {
+                                                                       error(e.getLocalizedMessage(),
+                                                                                       "IOException", e);
+                                                               }
                                                        } else {
                                                                addBookPane(typeF, true);
                                                        }
                                                } else {
-                                                       helper.createMenu(false);
+                                                       helper.createMenu(status);
                                                        validate();
 
                                                        String desc = Instance.getTransGui().getStringX(
@@ -255,8 +261,11 @@ class GuiReaderMainPanel extends JPanel {
         * @param listMode
         *            TRUE to get a listing of all the sources or authors, FALSE to
         *            get one icon per source or author
+        * 
+        * @throws IOException
+        *             in case of I/O error
         */
-       public void addBookPane(boolean type, boolean listMode) {
+       public void addBookPane(boolean type, boolean listMode) throws IOException {
                this.currentType = type;
                BasicLibrary lib = helper.getReader().getLibrary();
                if (type) {
@@ -352,11 +361,17 @@ class GuiReaderMainPanel extends JPanel {
                        List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
 
                        List<MetaData> metas;
-                       if (currentType) {
-                               metas = lib.getListBySource(value);
-                       } else {
-                               metas = lib.getListByAuthor(value);
+                       try {
+                               if (currentType) {
+                                       metas = lib.getListBySource(value);
+                               } else {
+                                       metas = lib.getListByAuthor(value);
+                               }
+                       } catch (IOException e) {
+                               error(e.getLocalizedMessage(), "IOException", e);
+                               metas = new ArrayList<MetaData>();
                        }
+
                        for (MetaData meta : metas) {
                                infos.add(GuiReaderBookInfo.fromMeta(meta));
                        }
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderNavBar.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderNavBar.java
new file mode 100644 (file)
index 0000000..099b3c8
--- /dev/null
@@ -0,0 +1,339 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.Color;
+import java.awt.LayoutManager;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import be.nikiroo.fanfix.Instance;
+
+/**
+ * A Swing-based navigation bar, that displays first/previous/next/last page
+ * buttons.
+ * 
+ * @author niki
+ */
+public class GuiReaderNavBar extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private JLabel label;
+       private int index = 0;
+       private int min = 0;
+       private int max = 0;
+       private JButton[] navButtons;
+       String extraLabel = null;
+
+       private List<ActionListener> listeners = new ArrayList<ActionListener>();
+
+       /**
+        * Create a new navigation bar.
+        * <p>
+        * The minimum must be lower or equal to the maximum.
+        * <p>
+        * Note than a max of "-1" means "infinite".
+        * 
+        * @param min
+        *            the minimum page number (cannot be negative)
+        * @param max
+        *            the maximum page number (cannot be lower than min, except if
+        *            -1 (infinite))
+        * 
+        * @throws IndexOutOfBoundsException
+        *             if min &gt; max and max is not "-1"
+        */
+       public GuiReaderNavBar(int min, int max) {
+               if (min > max && max != -1) {
+                       throw new IndexOutOfBoundsException(String.format(
+                                       "min (%d) > max (%d)", min, max));
+               }
+
+               LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS);
+               setLayout(layout);
+
+               navButtons = new JButton[4];
+
+               navButtons[0] = createNavButton("<<", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(GuiReaderNavBar.this.min);
+                               fireEvent();
+                       }
+               });
+               navButtons[1] = createNavButton(" < ", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(index - 1);
+                               fireEvent();
+                       }
+               });
+               navButtons[2] = createNavButton(" > ", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(index + 1);
+                               fireEvent();
+                       }
+               });
+               navButtons[3] = createNavButton(">>", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(GuiReaderNavBar.this.max);
+                               fireEvent();
+                       }
+               });
+
+               for (JButton navButton : navButtons) {
+                       add(navButton);
+               }
+
+               label = new JLabel("");
+               add(label);
+
+               this.min = min;
+               this.max = max;
+               this.index = min;
+
+               updateEnabled();
+               updateLabel();
+               fireEvent();
+       }
+
+       /**
+        * The current index, must be between {@link GuiReaderNavBar#min} and
+        * {@link GuiReaderNavBar#max}, both inclusive.
+        * 
+        * @return the index
+        */
+       public int getIndex() {
+               return index;
+       }
+
+       /**
+        * The current index, must be between {@link GuiReaderNavBar#min} and
+        * {@link GuiReaderNavBar#max}, both inclusive.
+        * 
+        * @param index
+        *            the new index
+        */
+       public void setIndex(int index) {
+               if (index != this.index) {
+                       if (index < min || (index > max && max != -1)) {
+                               throw new IndexOutOfBoundsException(String.format(
+                                               "Index %d but min/max is [%d/%d]", index, min, max));
+                       }
+
+                       this.index = index;
+                       updateLabel();
+               }
+
+               updateEnabled();
+       }
+
+       /**
+        * The minimun page number. Cannot be negative.
+        * 
+        * @return the min
+        */
+       public int getMin() {
+               return min;
+       }
+
+       /**
+        * The minimum page number. Cannot be negative.
+        * <p>
+        * May update the index if needed (if the index is &lt; the new min).
+        * <p>
+        * Will also (always) update the label and enable/disable the required
+        * buttons.
+        * 
+        * @param min
+        *            the new min
+        */
+       public void setMin(int min) {
+               this.min = min;
+               if (index < min) {
+                       index = min;
+               }
+               updateEnabled();
+               updateLabel();
+
+       }
+
+       /**
+        * The maximum page number. Cannot be lower than min, except if -1
+        * (infinite).
+        * 
+        * @return the max
+        */
+       public int getMax() {
+               return max;
+       }
+
+       /**
+        * The maximum page number. Cannot be lower than min, except if -1
+        * (infinite).
+        * <p>
+        * May update the index if needed (if the index is &gt; the new max).
+        * <p>
+        * Will also (always) update the label and enable/disable the required
+        * buttons.
+        * 
+        * @param max
+        *            the new max
+        */
+       public void setMax(int max) {
+               this.max = max;
+               if (index > max && max != -1) {
+                       index = max;
+               }
+               updateEnabled();
+               updateLabel();
+       }
+
+       /**
+        * The current extra label to display with the default
+        * {@link GuiReaderNavBar#computeLabel(int, int, int)} implementation.
+        * 
+        * @return the current label
+        */
+       public String getExtraLabel() {
+               return extraLabel;
+       }
+
+       /**
+        * The current extra label to display with the default
+        * {@link GuiReaderNavBar#computeLabel(int, int, int)} implementation.
+        * 
+        * @param currentLabel
+        *            the new current label
+        */
+       public void setExtraLabel(String currentLabel) {
+               this.extraLabel = currentLabel;
+               updateLabel();
+       }
+
+       /**
+        * Add a listener that will be called on each page change.
+        * 
+        * @param listener
+        *            the new listener
+        */
+       public void addActionListener(ActionListener listener) {
+               listeners.add(listener);
+       }
+
+       /**
+        * Remove the given listener if possible.
+        * 
+        * @param listener
+        *            the listener to remove
+        * @return TRUE if it was removed, FALSE if it was not found
+        */
+       public boolean removeActionListener(ActionListener listener) {
+               return listeners.remove(listener);
+       }
+
+       /**
+        * Remove all the listeners.
+        */
+       public void clearActionsListeners() {
+               listeners.clear();
+       }
+
+       /**
+        * Notify a change of page.
+        */
+       public void fireEvent() {
+               for (ActionListener listener : listeners) {
+                       try {
+                               listener.actionPerformed(new ActionEvent(this,
+                                               ActionEvent.ACTION_FIRST, "page changed"));
+                       } catch (Exception e) {
+                               Instance.getTraceHandler().error(e);
+                       }
+               }
+       }
+
+       /**
+        * Create a single navigation button.
+        * 
+        * @param text
+        *            the text to display
+        * @param action
+        *            the action to take on click
+        * @return the button
+        */
+       private JButton createNavButton(String text, ActionListener action) {
+               JButton navButton = new JButton(text);
+               navButton.addActionListener(action);
+               navButton.setForeground(Color.BLUE);
+               return navButton;
+       }
+
+       /**
+        * Update the label displayed in the UI.
+        */
+       private void updateLabel() {
+               label.setText(computeLabel(index, min, max));
+       }
+
+       /**
+        * Update the navigation buttons "enabled" state according to the current
+        * index value.
+        */
+       private void updateEnabled() {
+               navButtons[0].setEnabled(index > min);
+               navButtons[1].setEnabled(index > min);
+               navButtons[2].setEnabled(index < max || max == -1);
+               navButtons[3].setEnabled(index < max || max == -1);
+       }
+
+       /**
+        * Return the label to display for the given index.
+        * <p>
+        * Swing HTML (HTML3) is supported if surrounded by &lt;HTML&gt; and
+        * &lt;/HTML&gt;.
+        * <p>
+        * By default, return "Page 1/5: current_label" (with the current index and
+        * {@link GuiReaderNavBar#getCurrentLabel()}).
+        * 
+        * @param index
+        *            the new index number
+        * @param mix
+        *            the minimum index (inclusive)
+        * @param max
+        *            the maximum index (inclusive)
+        * @return the label
+        */
+       protected String computeLabel(int index,
+                       @SuppressWarnings("unused") int min, int max) {
+
+               String base = "&nbsp;&nbsp;<B>Page <SPAN COLOR='#444466'>%d</SPAN>&nbsp;";
+               if (max >= 0) {
+                       base += "/&nbsp;%d";
+               }
+               base += "</B>";
+
+               String ifLabel = ": %s";
+
+               String display = base;
+               String label = getExtraLabel();
+               if (label != null && !label.trim().isEmpty()) {
+                       display += ifLabel;
+               }
+
+               display = "<HTML>" + display + "</HTML>";
+
+               if (max >= 0) {
+                       return String.format(display, index, max, label);
+               }
+
+               return String.format(display, index, label);
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchAction.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchAction.java
new file mode 100644 (file)
index 0000000..b3c8f8b
--- /dev/null
@@ -0,0 +1,91 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.net.URL;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.library.BasicLibrary;
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.ui.ProgressBar;
+
+public class GuiReaderSearchAction extends JFrame {
+       private static final long serialVersionUID = 1L;
+
+       private GuiReaderBookInfo info;
+       private ProgressBar pgBar;
+
+       public GuiReaderSearchAction(BasicLibrary lib, GuiReaderBookInfo info) {
+               super(info.getMainInfo());
+               this.setSize(800, 600);
+               this.info = info;
+
+               setLayout(new BorderLayout());
+
+               JPanel main = new JPanel(new BorderLayout());
+               JPanel props = new GuiReaderPropertiesPane(lib, info.getMeta());
+
+               main.add(props, BorderLayout.NORTH);
+               main.add(new GuiReaderViewerPanel(info.getMeta(), info.getMeta()
+                               .isImageDocument()), BorderLayout.CENTER);
+               main.add(createImportButton(lib), BorderLayout.SOUTH);
+
+               add(main, BorderLayout.CENTER);
+
+               pgBar = new ProgressBar();
+               pgBar.setVisible(false);
+               add(pgBar, BorderLayout.SOUTH);
+
+               pgBar.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               pgBar.invalidate();
+                               pgBar.setProgress(null);
+                               setEnabled(true);
+                               validate();
+                       }
+               });
+
+               pgBar.addUpdateListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               pgBar.invalidate();
+                               validate();
+                               repaint();
+                       }
+               });
+       }
+
+       private Component createImportButton(final BasicLibrary lib) {
+               JButton imprt = new JButton("Import into library");
+               imprt.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent ae) {
+                               final Progress pg = new Progress();
+                               pgBar.setProgress(pg);
+
+                               new Thread(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               try {
+                                                       lib.imprt(new URL(info.getMeta().getUrl()), null);
+                                               } catch (IOException e) {
+                                                       Instance.getTraceHandler().error(e);
+                                               }
+
+                                               pg.done();
+                                       }
+                               }).start();
+                       }
+               });
+
+               return imprt;
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByNamePanel.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByNamePanel.java
new file mode 100644 (file)
index 0000000..ebdb21a
--- /dev/null
@@ -0,0 +1,246 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.reader.ui.GuiReaderSearchByPanel.Waitable;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+
+/**
+ * This panel represents a search panel that works for keywords and tags based
+ * searches.
+ * 
+ * @author niki
+ */
+public class GuiReaderSearchByNamePanel extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private BasicSearchable searchable;
+
+       private JTextField keywordsField;
+       private JButton submitKeywords;
+
+       private int page;
+       private int maxPage;
+       private List<MetaData> stories = new ArrayList<MetaData>();
+       private int storyItem;
+
+       public GuiReaderSearchByNamePanel(final Waitable waitable) {
+               super(new BorderLayout());
+
+               keywordsField = new JTextField();
+               add(keywordsField, BorderLayout.CENTER);
+
+               submitKeywords = new JButton("Search");
+               add(submitKeywords, BorderLayout.EAST);
+
+               // should be done out of UI
+               final Runnable go = new Runnable() {
+                       @Override
+                       public void run() {
+                               waitable.setWaiting(true);
+                               try {
+                                       search(keywordsField.getText(), 1, 0);
+                                       waitable.fireEvent();
+                               } finally {
+                                       waitable.setWaiting(false);
+                               }
+                       }
+               };
+
+               keywordsField.addKeyListener(new KeyAdapter() {
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                               if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+                                       new Thread(go).start();
+                               } else {
+                                       super.keyReleased(e);
+                               }
+                       }
+               });
+
+               submitKeywords.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               new Thread(go).start();
+                       }
+               });
+
+               setSearchable(null);
+       }
+
+       /**
+        * The {@link BasicSearchable} object use for the searches themselves.
+        * <p>
+        * Can be NULL, but no searches will work.
+        * 
+        * @param searchable
+        *            the new searchable
+        */
+       public void setSearchable(BasicSearchable searchable) {
+               this.searchable = searchable;
+               page = 0;
+               maxPage = -1;
+               storyItem = 0;
+               stories = new ArrayList<MetaData>();
+               updateKeywords("");
+       }
+
+       /**
+        * The currently displayed page of result for the current search (see the
+        * <tt>page</tt> parameter of
+        * {@link GuiReaderSearchByNamePanel#search(String, int, int)}).
+        * 
+        * @return the currently displayed page of results
+        */
+       public int getPage() {
+               return page;
+       }
+
+       /**
+        * The number of pages of result for the current search (see the
+        * <tt>page</tt> parameter of
+        * {@link GuiReaderSearchByPanel#search(String, int, int)}).
+        * <p>
+        * For an unknown number or when not applicable, -1 is returned.
+        * 
+        * @return the number of pages of results or -1
+        */
+       public int getMaxPage() {
+               return maxPage;
+       }
+
+       /**
+        * Return the keywords used for the current search.
+        * 
+        * @return the keywords
+        */
+       public String getCurrentKeywords() {
+               return keywordsField.getText();
+       }
+
+       /**
+        * The currently loaded stories (the result of the latest search).
+        * 
+        * @return the stories
+        */
+       public List<MetaData> getStories() {
+               return stories;
+       }
+
+       /**
+        * Return the currently selected story (the <tt>item</tt>) if it was
+        * specified in the latest, or 0 if not.
+        * <p>
+        * Note: this is thus a 1-based index, <b>not</b> a 0-based index.
+        * 
+        * @return the item
+        */
+       public int getStoryItem() {
+               return storyItem;
+       }
+
+       /**
+        * Update the keywords displayed on screen.
+        * 
+        * @param keywords
+        *            the keywords
+        */
+       private void updateKeywords(final String keywords) {
+               if (!keywords.equals(keywordsField.getText())) {
+                       GuiReaderSearchFrame.inUi(new Runnable() {
+                               @Override
+                               public void run() {
+                                       keywordsField.setText(keywords);
+                               }
+                       });
+               }
+       }
+
+       /**
+        * Search for the given terms on the currently selected searchable.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param keywords
+        *            the keywords to search for
+        * @param page
+        *            the page of results to load
+        * @param item
+        *            the item to select (or 0 for none by default)
+        * 
+        * @throw IndexOutOfBoundsException if the page is out of bounds
+        */
+       public void search(String keywords, int page, int item) {
+               List<MetaData> stories = new ArrayList<MetaData>();
+               int storyItem = 0;
+
+               updateKeywords(keywords);
+
+               int maxPage = -1;
+               if (searchable != null) {
+                       try {
+                               maxPage = searchable.searchPages(keywords);
+                       } catch (IOException e) {
+                               GuiReaderSearchFrame.error(e);
+                       }
+               }
+
+               if (page > 0) {
+                       if (maxPage >= 0 && (page <= 0 || page > maxPage)) {
+                               throw new IndexOutOfBoundsException("Page " + page + " out of "
+                                               + maxPage);
+                       }
+
+                       if (searchable != null) {
+                               try {
+                                       stories = searchable.search(keywords, page);
+                               } catch (IOException e) {
+                                       GuiReaderSearchFrame.error(e);
+                               }
+                       }
+
+                       if (item > 0 && item <= stories.size()) {
+                               storyItem = item;
+                       } else if (item > 0) {
+                               GuiReaderSearchFrame.error(String.format(
+                                               "Story item does not exist: Search [%s], item %d",
+                                               keywords, item));
+                       }
+               }
+
+               this.page = page;
+               this.maxPage = maxPage;
+               this.stories = stories;
+               this.storyItem = storyItem;
+       }
+
+       /**
+        * Enables or disables this component, depending on the value of the
+        * parameter <code>b</code>. An enabled component can respond to user input
+        * and generate events. Components are enabled initially by default.
+        * <p>
+        * Disabling this component will also affect its children.
+        * 
+        * @param b
+        *            If <code>true</code>, this component is enabled; otherwise
+        *            this component is disabled
+        */
+       @Override
+       public void setEnabled(boolean b) {
+               super.setEnabled(b);
+               keywordsField.setEnabled(b);
+               submitKeywords.setEnabled(b);
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java
new file mode 100644 (file)
index 0000000..8f95d4c
--- /dev/null
@@ -0,0 +1,281 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+import be.nikiroo.fanfix.searchable.SearchableTag;
+import be.nikiroo.fanfix.supported.SupportType;
+
+/**
+ * This panel represents a search panel that works for keywords and tags based
+ * searches.
+ * 
+ * @author niki
+ */
+public class GuiReaderSearchByPanel extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private Waitable waitable;
+
+       private boolean searchByTags;
+       private JTabbedPane searchTabs;
+       private GuiReaderSearchByNamePanel byName;
+       private GuiReaderSearchByTagPanel byTag;
+
+       /**
+        * This interface represents an item that wan be put in "wait" mode. It is
+        * supposed to be used for long running operations during which we want to
+        * disable UI interactions.
+        * <p>
+        * It also allows reporting an event to the item.
+        * 
+        * @author niki
+        */
+       public interface Waitable {
+               /**
+                * Set the item in wait mode, blocking it from accepting UI input.
+                * 
+                * @param waiting
+                *            TRUE for wait more, FALSE to restore normal mode
+                */
+               public void setWaiting(boolean waiting);
+
+               /**
+                * Notify the {@link Waitable} that an event occured (i.e., new stories
+                * were found).
+                */
+               public void fireEvent();
+       }
+
+       /**
+        * Create a new {@link GuiReaderSearchByPanel}.
+        * 
+        * @param waitable
+        *            the waitable we can wait on for long UI operations
+        */
+       public GuiReaderSearchByPanel(Waitable waitable) {
+               setLayout(new BorderLayout());
+
+               this.waitable = waitable;
+               searchByTags = false;
+
+               byName = new GuiReaderSearchByNamePanel(waitable);
+               byTag = new GuiReaderSearchByTagPanel(waitable);
+
+               searchTabs = new JTabbedPane();
+               searchTabs.addTab("By name", byName);
+               searchTabs.addTab("By tags", byTag);
+               searchTabs.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               searchByTags = (searchTabs.getSelectedComponent() == byTag);
+                       }
+               });
+
+               add(searchTabs, BorderLayout.CENTER);
+               updateSearchBy(searchByTags);
+       }
+
+       /**
+        * Set the new {@link SupportType}.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * <p>
+        * Note that if a non-searchable {@link SupportType} is used, an
+        * {@link IllegalArgumentException} will be thrown.
+        * 
+        * @param supportType
+        *            the support mode, must be searchable or NULL
+        * 
+        * @throws IllegalArgumentException
+        *             if the {@link SupportType} is not NULL but not searchable
+        *             (see {@link BasicSearchable#getSearchable(SupportType)})
+        */
+       public void setSupportType(SupportType supportType) {
+               BasicSearchable searchable = BasicSearchable.getSearchable(supportType);
+               if (searchable == null && supportType != null) {
+                       throw new IllegalArgumentException("Unupported support type: "
+                                       + supportType);
+               }
+
+               byName.setSearchable(searchable);
+               byTag.setSearchable(searchable);
+       }
+
+       /**
+        * The currently displayed page of result for the current search (see the
+        * <tt>page</tt> parameter of
+        * {@link GuiReaderSearchByPanel#search(String, int, int)} or
+        * {@link GuiReaderSearchByPanel#searchTag(SupportType, int, int, SearchableTag)}
+        * ).
+        * 
+        * @return the currently displayed page of results
+        */
+       public int getPage() {
+               if (!searchByTags) {
+                       return byName.getPage();
+               }
+
+               return byTag.getPage();
+       }
+
+       /**
+        * The number of pages of result for the current search (see the
+        * <tt>page</tt> parameter of
+        * {@link GuiReaderSearchByPanel#search(String, int, int)} or
+        * {@link GuiReaderSearchByPanel#searchTag(SupportType, int, int, SearchableTag)}
+        * ).
+        * <p>
+        * For an unknown number or when not applicable, -1 is returned.
+        * 
+        * @return the number of pages of results or -1
+        */
+       public int getMaxPage() {
+               if (!searchByTags) {
+                       return byName.getMaxPage();
+               }
+
+               return byTag.getMaxPage();
+       }
+
+       /**
+        * Set the page of results to display for the current search. This will
+        * cause {@link Waitable#fireEvent()} to be called if needed.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param page
+        *            the page of results to set
+        * 
+        * @throw IndexOutOfBoundsException if the page is out of bounds
+        */
+       public void setPage(int page) {
+               if (searchByTags) {
+                       searchTag(byTag.getCurrentTag(), page, 0);
+               } else {
+                       search(byName.getCurrentKeywords(), page, 0);
+               }
+       }
+
+       /**
+        * The currently loaded stories (the result of the latest search).
+        * 
+        * @return the stories
+        */
+       public List<MetaData> getStories() {
+               if (!searchByTags) {
+                       return byName.getStories();
+               }
+
+               return byTag.getStories();
+       }
+
+       /**
+        * Return the currently selected story (the <tt>item</tt>) if it was
+        * specified in the latest, or 0 if not.
+        * <p>
+        * Note: this is thus a 1-based index, <b>not</b> a 0-based index.
+        * 
+        * @return the item
+        */
+       public int getStoryItem() {
+               if (!searchByTags) {
+                       return byName.getStoryItem();
+               }
+
+               return byTag.getStoryItem();
+       }
+
+       /**
+        * Update the kind of searches to make: search by keywords or search by tags
+        * (it will impact what the user can see and interact with on the UI).
+        * 
+        * @param byTag
+        *            TRUE for tag-based searches, FALSE for keywords-based searches
+        */
+       private void updateSearchBy(final boolean byTag) {
+               GuiReaderSearchFrame.inUi(new Runnable() {
+                       @Override
+                       public void run() {
+                               if (!byTag) {
+                                       searchTabs.setSelectedIndex(0);
+                               } else {
+                                       searchTabs.setSelectedIndex(1);
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Search for the given terms on the currently selected searchable. This
+        * will cause {@link Waitable#fireEvent()} to be called if needed.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param keywords
+        *            the keywords to search for
+        * @param page
+        *            the page of results to load
+        * @param item
+        *            the item to select (or 0 for none by default)
+        * 
+        * @throw IndexOutOfBoundsException if the page is out of bounds
+        */
+       public void search(final String keywords, final int page, final int item) {
+               updateSearchBy(false);
+               byName.search(keywords, page, item);
+               waitable.fireEvent();
+       }
+
+       /**
+        * Search for the given tag on the currently selected searchable. This will
+        * cause {@link Waitable#fireEvent()} to be called if needed.
+        * <p>
+        * If the tag contains children tags, those will be displayed so you can
+        * select them; if the tag is a leaf tag, the linked stories will be
+        * displayed.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param tag
+        *            the tag to search for, or NULL for base tags
+        * @param page
+        *            the page of results to load
+        * @param item
+        *            the item to select (or 0 for none by default)
+        * 
+        * @throw IndexOutOfBoundsException if the page is out of bounds
+        */
+       public void searchTag(final SearchableTag tag, final int page,
+                       final int item) {
+               updateSearchBy(true);
+               byTag.searchTag(tag, page, item);
+               waitable.fireEvent();
+       }
+
+       /**
+        * Enables or disables this component, depending on the value of the
+        * parameter <code>b</code>. An enabled component can respond to user input
+        * and generate events. Components are enabled initially by default.
+        * <p>
+        * Disabling this component will also affect its children.
+        * 
+        * @param b
+        *            If <code>true</code>, this component is enabled; otherwise
+        *            this component is disabled
+        */
+       @Override
+       public void setEnabled(boolean b) {
+               super.setEnabled(b);
+               searchTabs.setEnabled(b);
+               byName.setEnabled(b);
+               byTag.setEnabled(b);
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java
new file mode 100644 (file)
index 0000000..260fc48
--- /dev/null
@@ -0,0 +1,458 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.JComboBox;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.ListCellRenderer;
+
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.reader.ui.GuiReaderSearchByPanel.Waitable;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+import be.nikiroo.fanfix.searchable.SearchableTag;
+import be.nikiroo.fanfix.supported.SupportType;
+
+/**
+ * This panel represents a search panel that works for keywords and tags based
+ * searches.
+ * 
+ * @author niki
+ */
+// JCombobox<E> not 1.6 compatible
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public class GuiReaderSearchByTagPanel extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private BasicSearchable searchable;
+       private Waitable waitable;
+
+       private SearchableTag currentTag;
+       private JPanel tagBars;
+       private List<JComboBox> combos;
+
+       private int page;
+       private int maxPage;
+       private List<MetaData> stories = new ArrayList<MetaData>();
+       private int storyItem;
+
+       public GuiReaderSearchByTagPanel(Waitable waitable) {
+               setLayout(new BorderLayout());
+
+               this.waitable = waitable;
+               combos = new ArrayList<JComboBox>();
+               page = 0;
+               maxPage = -1;
+
+               tagBars = new JPanel();
+               tagBars.setLayout(new BoxLayout(tagBars, BoxLayout.Y_AXIS));
+               add(tagBars, BorderLayout.NORTH);
+       }
+
+       /**
+        * The {@link BasicSearchable} object use for the searches themselves.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * <p>
+        * Can be NULL, but no searches will work.
+        * 
+        * @param searchable
+        *            the new searchable
+        */
+       public void setSearchable(BasicSearchable searchable) {
+               this.searchable = searchable;
+               page = 0;
+               maxPage = -1;
+               storyItem = 0;
+               stories = new ArrayList<MetaData>();
+               updateTags(null);
+       }
+
+       /**
+        * The currently displayed page of result for the current search (see the
+        * <tt>page</tt> parameter of
+        * {@link GuiReaderSearchByTagPanel#searchTag(SupportType, int, int, SearchableTag)}
+        * ).
+        * 
+        * @return the currently displayed page of results
+        */
+       public int getPage() {
+               return page;
+       }
+
+       /**
+        * The number of pages of result for the current search (see the
+        * <tt>page</tt> parameter of
+        * {@link GuiReaderSearchByPanel#searchTag(SupportType, int, int, SearchableTag)}
+        * ).
+        * <p>
+        * For an unknown number or when not applicable, -1 is returned.
+        * 
+        * @return the number of pages of results or -1
+        */
+       public int getMaxPage() {
+               return maxPage;
+       }
+
+       /**
+        * Return the tag used for the current search.
+        * 
+        * @return the tag (which can be NULL, for "base tags")
+        */
+       public SearchableTag getCurrentTag() {
+               return currentTag;
+       }
+
+       /**
+        * The currently loaded stories (the result of the latest search).
+        * 
+        * @return the stories
+        */
+       public List<MetaData> getStories() {
+               return stories;
+       }
+
+       /**
+        * Return the currently selected story (the <tt>item</tt>) if it was
+        * specified in the latest, or 0 if not.
+        * <p>
+        * Note: this is thus a 1-based index, <b>not</b> a 0-based index.
+        * 
+        * @return the item
+        */
+       public int getStoryItem() {
+               return storyItem;
+       }
+
+       /**
+        * Update the tags displayed on screen and reset the tags bar.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param tag
+        *            the tag to use, or NULL for base tags
+        */
+       private void updateTags(final SearchableTag tag) {
+               final List<SearchableTag> parents = new ArrayList<SearchableTag>();
+               SearchableTag parent = (tag == null) ? null : tag;
+               while (parent != null) {
+                       parents.add(parent);
+                       parent = parent.getParent();
+               }
+
+               List<SearchableTag> rootTags = new ArrayList<SearchableTag>();
+               SearchableTag selectedRootTag = null;
+               selectedRootTag = parents.isEmpty() ? null : parents
+                               .get(parents.size() - 1);
+
+               if (searchable != null) {
+                       try {
+                               rootTags = searchable.getTags();
+                       } catch (IOException e) {
+                               GuiReaderSearchFrame.error(e);
+                       }
+               }
+
+               final List<SearchableTag> rootTagsF = rootTags;
+               final SearchableTag selectedRootTagF = selectedRootTag;
+
+               GuiReaderSearchFrame.inUi(new Runnable() {
+                       @Override
+                       public void run() {
+                               tagBars.invalidate();
+                               tagBars.removeAll();
+
+                               addTagBar(rootTagsF, selectedRootTagF);
+
+                               for (int i = parents.size() - 1; i >= 0; i--) {
+                                       SearchableTag selectedChild = null;
+                                       if (i > 0) {
+                                               selectedChild = parents.get(i - 1);
+                                       }
+
+                                       SearchableTag parent = parents.get(i);
+                                       addTagBar(parent.getChildren(), selectedChild);
+                               }
+
+                               tagBars.validate();
+                       }
+               });
+       }
+
+       /**
+        * Add a tags bar (do not remove possible previous ones).
+        * <p>
+        * Will always add an "empty" (NULL) tag as first option.
+        * 
+        * @param tags
+        *            the tags to display
+        * @param selected
+        *            the selected tag if any, or NULL for none
+        */
+       private void addTagBar(List<SearchableTag> tags,
+                       final SearchableTag selected) {
+               tags.add(0, null);
+
+               final int comboIndex = combos.size();
+
+               final JComboBox combo = new JComboBox(
+                               tags.toArray(new SearchableTag[] {}));
+               combo.setSelectedItem(selected);
+
+               final ListCellRenderer basic = combo.getRenderer();
+
+               combo.setRenderer(new ListCellRenderer() {
+                       @Override
+                       public Component getListCellRendererComponent(JList list,
+                                       Object value, int index, boolean isSelected,
+                                       boolean cellHasFocus) {
+
+                               Object displayValue = value;
+                               if (value instanceof SearchableTag) {
+                                       displayValue = ((SearchableTag) value).getName();
+                               } else {
+                                       displayValue = "Select a tag...";
+                                       cellHasFocus = false;
+                                       isSelected = false;
+                               }
+
+                               Component rep = basic.getListCellRendererComponent(list,
+                                               displayValue, index, isSelected, cellHasFocus);
+
+                               if (value == null) {
+                                       rep.setForeground(Color.GRAY);
+                               }
+
+                               return rep;
+                       }
+               });
+
+               combo.addActionListener(createComboTagAction(comboIndex));
+
+               combos.add(combo);
+               tagBars.add(combo);
+       }
+
+       /**
+        * The action to do on {@link JComboBox} selection.
+        * <p>
+        * The content of the action is:
+        * <ul>
+        * <li>Remove all tags bar below this one</li>
+        * <li>Load the subtags if any in anew tags bar</li>
+        * <li>Load the related stories if the tag was a leaf tag and notify the
+        * {@link Waitable} (via {@link Waitable#fireEvent()})</li>
+        * </ul>
+        * 
+        * @param comboIndex
+        *            the index of the related {@link JComboBox}
+        * 
+        * @return the action
+        */
+       private ActionListener createComboTagAction(final int comboIndex) {
+               return new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent ae) {
+                               List<JComboBox> combos = GuiReaderSearchByTagPanel.this.combos;
+                               if (combos == null || comboIndex < 0
+                                               || comboIndex >= combos.size()) {
+                                       return;
+                               }
+
+                               // Tag can be NULL
+                               final SearchableTag tag = (SearchableTag) combos
+                                               .get(comboIndex).getSelectedItem();
+
+                               while (comboIndex + 1 < combos.size()) {
+                                       JComboBox combo = combos.remove(comboIndex + 1);
+                                       tagBars.remove(combo);
+                               }
+
+                               new Thread(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               waitable.setWaiting(true);
+                                               try {
+                                                       final List<SearchableTag> children = getChildrenForTag(tag);
+                                                       if (children != null) {
+                                                               GuiReaderSearchFrame.inUi(new Runnable() {
+                                                                       @Override
+                                                                       public void run() {
+                                                                               addTagBar(children, tag);
+                                                                       }
+                                                               });
+                                                       }
+
+                                                       if (tag != null && tag.isLeaf()) {
+                                                               storyItem = 0;
+                                                               try {
+                                                                       searchable.fillTag(tag);
+                                                                       page = 1;
+                                                                       stories = searchable.search(tag, 1);
+                                                                       maxPage = searchable.searchPages(tag);
+                                                                       currentTag = tag;
+                                                               } catch (IOException e) {
+                                                                       GuiReaderSearchFrame.error(e);
+                                                                       page = 0;
+                                                                       maxPage = -1;
+                                                                       stories = new ArrayList<MetaData>();
+                                                               }
+
+                                                               waitable.fireEvent();
+                                                       }
+                                               } finally {
+                                                       waitable.setWaiting(false);
+                                               }
+                                       }
+                               }).start();
+                       }
+               };
+       }
+
+       /**
+        * Get the children of the given tag (or the base tags if the given tag is
+        * NULL).
+        * <p>
+        * This action will "fill" ({@link BasicSearchable#fillTag(SearchableTag)})
+        * the given tag if needed first.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param tag
+        *            the tag to search into or NULL for the base tags
+        * @return the children
+        */
+       private List<SearchableTag> getChildrenForTag(final SearchableTag tag) {
+               List<SearchableTag> children = new ArrayList<SearchableTag>();
+               if (tag == null) {
+                       try {
+                               List<SearchableTag> baseTags = searchable.getTags();
+                               children = baseTags;
+                       } catch (IOException e) {
+                               GuiReaderSearchFrame.error(e);
+                       }
+               } else {
+                       try {
+                               searchable.fillTag(tag);
+                       } catch (IOException e) {
+                               GuiReaderSearchFrame.error(e);
+                       }
+
+                       if (!tag.isLeaf()) {
+                               children = tag.getChildren();
+                       } else {
+                               children = null;
+                       }
+               }
+
+               return children;
+       }
+
+       /**
+        * Search for the given tag on the currently selected searchable.
+        * <p>
+        * If the tag contains children tags, those will be displayed so you can
+        * select them; if the tag is a leaf tag, the linked stories will be
+        * displayed.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param tag
+        *            the tag to search for, or NULL for base tags
+        * @param page
+        *            the page of results to load
+        * @param item
+        *            the item to select (or 0 for none by default)
+        * 
+        * @throw IndexOutOfBoundsException if the page is out of bounds
+        */
+       public void searchTag(SearchableTag tag, int page, int item) {
+               List<MetaData> stories = new ArrayList<MetaData>();
+               int storyItem = 0;
+
+               currentTag = tag;
+               updateTags(tag);
+
+               int maxPage = -1;
+               if (tag != null) {
+                       try {
+                               searchable.fillTag(tag);
+
+                               if (!tag.isLeaf()) {
+                                       List<SearchableTag> subtags = tag.getChildren();
+                                       if (item > 0 && item <= subtags.size()) {
+                                               SearchableTag subtag = subtags.get(item - 1);
+                                               try {
+                                                       tag = subtag;
+                                                       searchable.fillTag(tag);
+                                               } catch (IOException e) {
+                                                       GuiReaderSearchFrame.error(e);
+                                               }
+                                       } else if (item > 0) {
+                                               GuiReaderSearchFrame.error(String.format(
+                                                               "Tag item does not exist: Tag [%s], item %d",
+                                                               tag.getFqName(), item));
+                                       }
+                               }
+
+                               maxPage = searchable.searchPages(tag);
+                               if (page > 0 && tag.isLeaf()) {
+                                       if (maxPage >= 0 && (page <= 0 || page > maxPage)) {
+                                               throw new IndexOutOfBoundsException("Page " + page
+                                                               + " out of " + maxPage);
+                                       }
+
+                                       try {
+                                               stories = searchable.search(tag, page);
+                                               if (item > 0 && item <= stories.size()) {
+                                                       storyItem = item;
+                                               } else if (item > 0) {
+                                                       GuiReaderSearchFrame
+                                                                       .error(String
+                                                                                       .format("Story item does not exist: Tag [%s], item %d",
+                                                                                                       tag.getFqName(), item));
+                                               }
+                                       } catch (IOException e) {
+                                               GuiReaderSearchFrame.error(e);
+                                       }
+                               }
+                       } catch (IOException e) {
+                               GuiReaderSearchFrame.error(e);
+                               maxPage = 0;
+                       }
+               }
+
+               this.stories = stories;
+               this.storyItem = storyItem;
+               this.page = page;
+               this.maxPage = maxPage;
+       }
+
+       /**
+        * Enables or disables this component, depending on the value of the
+        * parameter <code>b</code>. An enabled component can respond to user input
+        * and generate events. Components are enabled initially by default.
+        * <p>
+        * Disabling this component will also affect its children.
+        * 
+        * @param b
+        *            If <code>true</code>, this component is enabled; otherwise
+        *            this component is disabled
+        */
+       @Override
+       public void setEnabled(boolean b) {
+               super.setEnabled(b);
+               tagBars.setEnabled(b);
+               for (JComboBox combo : combos) {
+                       combo.setEnabled(b);
+               }
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchFrame.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchFrame.java
new file mode 100644 (file)
index 0000000..5b99772
--- /dev/null
@@ -0,0 +1,380 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.EventQueue;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
+import be.nikiroo.fanfix.searchable.BasicSearchable;
+import be.nikiroo.fanfix.searchable.SearchableTag;
+import be.nikiroo.fanfix.supported.SupportType;
+
+/**
+ * This frame will allow you to search through the supported websites for new
+ * stories/comics.
+ * 
+ * @author niki
+ */
+// JCombobox<E> not 1.6 compatible
+@SuppressWarnings({ "unchecked", "rawtypes" })
+public class GuiReaderSearchFrame extends JFrame {
+       private static final long serialVersionUID = 1L;
+
+       private List<SupportType> supportTypes;
+
+       private JComboBox comboSupportTypes;
+       private ActionListener comboSupportTypesListener;
+       private GuiReaderSearchByPanel searchPanel;
+       private GuiReaderNavBar navbar;
+
+       private boolean seeWordcount;
+       private GuiReaderGroup books;
+
+       public GuiReaderSearchFrame(final GuiReader reader) {
+               super("Browse stories");
+               setLayout(new BorderLayout());
+               setSize(800, 600);
+
+               supportTypes = new ArrayList<SupportType>();
+               supportTypes.add(null);
+               for (SupportType type : SupportType.values()) {
+                       if (BasicSearchable.getSearchable(type) != null) {
+                               supportTypes.add(type);
+                       }
+               }
+
+               comboSupportTypes = new JComboBox(
+                               supportTypes.toArray(new SupportType[] {}));
+
+               comboSupportTypesListener = new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               final SupportType support = (SupportType) comboSupportTypes
+                                               .getSelectedItem();
+                               setWaiting(true);
+                               new Thread(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               try {
+                                                       updateSupportType(support);
+                                               } finally {
+                                                       setWaiting(false);
+                                               }
+                                       }
+                               }).start();
+                       }
+               };
+               comboSupportTypes.addActionListener(comboSupportTypesListener);
+
+               JPanel searchSites = new JPanel(new BorderLayout());
+               searchSites.add(comboSupportTypes, BorderLayout.CENTER);
+               searchSites.add(new JLabel(" " + "Website : "), BorderLayout.WEST);
+
+               searchPanel = new GuiReaderSearchByPanel(
+                               new GuiReaderSearchByPanel.Waitable() {
+                                       @Override
+                                       public void setWaiting(boolean waiting) {
+                                               GuiReaderSearchFrame.this.setWaiting(waiting);
+                                       }
+
+                                       @Override
+                                       public void fireEvent() {
+                                               updatePages(searchPanel.getPage(),
+                                                               searchPanel.getMaxPage());
+                                               List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
+                                               for (MetaData meta : searchPanel.getStories()) {
+                                                       infos.add(GuiReaderBookInfo.fromMeta(meta));
+                                               }
+
+                                               int page = searchPanel.getPage();
+                                               if (page <= 0) {
+                                                       navbar.setMin(1);
+                                                       navbar.setMax(1);
+                                               } else {
+                                                       int max = searchPanel.getMaxPage();
+                                                       navbar.setMin(1);
+                                                       navbar.setMax(max);
+                                                       navbar.setIndex(page);
+                                               }
+                                               updateBooks(infos);
+
+                                               // ! 1-based index !
+                                               int item = searchPanel.getStoryItem();
+                                               if (item > 0 && item <= books.getBooksCount()) {
+                                                       books.setSelectedBook(item - 1, false);
+                                               }
+                                       }
+                               });
+
+               JPanel top = new JPanel(new BorderLayout());
+               top.add(searchSites, BorderLayout.NORTH);
+               top.add(searchPanel, BorderLayout.CENTER);
+
+               add(top, BorderLayout.NORTH);
+
+               books = new GuiReaderGroup(reader, null, null);
+               books.setActionListener(new BookActionListener() {
+                       @Override
+                       public void select(GuiReaderBook book) {
+                       }
+
+                       @Override
+                       public void popupRequested(GuiReaderBook book, Component target,
+                                       int x, int y) {
+                       }
+
+                       @Override
+                       public void action(GuiReaderBook book) {
+                               new GuiReaderSearchAction(reader.getLibrary(), book.getInfo())
+                                               .setVisible(true);
+                       }
+               });
+               JScrollPane scroll = new JScrollPane(books);
+               scroll.getVerticalScrollBar().setUnitIncrement(16);
+               add(scroll, BorderLayout.CENTER);
+
+               navbar = new GuiReaderNavBar(-1, -1) {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected String computeLabel(int index, int min, int max) {
+                               if (index <= 0) {
+                                       return "";
+                               }
+                               return super.computeLabel(index, min, max);
+                       }
+               };
+
+               navbar.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               searchPanel.setPage(navbar.getIndex());
+                       }
+               });
+
+               add(navbar, BorderLayout.SOUTH);
+       }
+
+       /**
+        * Update the {@link SupportType} currently displayed to the user.
+        * <p>
+        * Will also cause a search for the new base tags of the given support if
+        * not NULL.
+        * <p>
+        * This operation can be long and should be run outside the UI thread.
+        * 
+        * @param supportType
+        *            the new {@link SupportType}
+        */
+       private void updateSupportType(final SupportType supportType) {
+               inUi(new Runnable() {
+                       @Override
+                       public void run() {
+                               books.clear();
+
+                               comboSupportTypes
+                                               .removeActionListener(comboSupportTypesListener);
+                               comboSupportTypes.setSelectedItem(supportType);
+                               comboSupportTypes.addActionListener(comboSupportTypesListener);
+                       }
+               });
+
+               searchPanel.setSupportType(supportType);
+       }
+
+       /**
+        * Update the pages and the lined buttons currently displayed on screen.
+        * <p>
+        * Those are the same pages and maximum pages used by
+        * {@link GuiReaderSearchByPanel#search(String, int, int)} and
+        * {@link GuiReaderSearchByPanel#searchTag(SearchableTag, int, int)}.
+        * 
+        * @param page
+        *            the current page of results
+        * @param maxPage
+        *            the maximum number of pages of results
+        */
+       private void updatePages(final int page, final int maxPage) {
+               inUi(new Runnable() {
+                       @Override
+                       public void run() {
+                               if (maxPage >= 1) {
+                                       navbar.setMin(1);
+                                       navbar.setMax(maxPage);
+                                       navbar.setIndex(page);
+                               } else {
+                                       navbar.setMin(-1);
+                                       navbar.setMax(-1);
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Update the currently displayed books.
+        * 
+        * @param infos
+        *            the new books
+        */
+       private void updateBooks(final List<GuiReaderBookInfo> infos) {
+               inUi(new Runnable() {
+                       @Override
+                       public void run() {
+                               books.refreshBooks(infos, seeWordcount);
+                       }
+               });
+       }
+
+       /**
+        * Search for the given terms on the currently selected searchable. This
+        * will update the displayed books if needed.
+        * <p>
+        * This operation is asynchronous.
+        * 
+        * @param keywords
+        *            the keywords to search for
+        * @param page
+        *            the page of results to load
+        * @param item
+        *            the item to select (or 0 for none by default)
+        */
+       public void search(final SupportType searchOn, final String keywords,
+                       final int page, final int item) {
+               setWaiting(true);
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       updateSupportType(searchOn);
+                                       searchPanel.search(keywords, page, item);
+                               } finally {
+                                       setWaiting(false);
+                               }
+                       }
+               }).start();
+       }
+
+       /**
+        * Search for the given tag on the currently selected searchable. This will
+        * update the displayed books if needed.
+        * <p>
+        * If the tag contains children tags, those will be displayed so you can
+        * select them; if the tag is a leaf tag, the linked stories will be
+        * displayed.
+        * <p>
+        * This operation is asynchronous.
+        * 
+        * @param tag
+        *            the tag to search for, or NULL for base tags
+        * @param page
+        *            the page of results to load
+        * @param item
+        *            the item to select (or 0 for none by default)
+        */
+       public void searchTag(final SupportType searchOn, final int page,
+                       final int item, final SearchableTag tag) {
+               setWaiting(true);
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       updateSupportType(searchOn);
+                                       searchPanel.searchTag(tag, page, item);
+                               } finally {
+                                       setWaiting(false);
+                               }
+                       }
+               }).start();
+       }
+
+       /**
+        * Process the given action in the main Swing UI thread.
+        * <p>
+        * The code will make sure the current thread is the main UI thread and, if
+        * not, will switch to it before executing the runnable.
+        * <p>
+        * Synchronous operation.
+        * 
+        * @param run
+        *            the action to run
+        */
+       static void inUi(final Runnable run) {
+               if (EventQueue.isDispatchThread()) {
+                       run.run();
+               } else {
+                       try {
+                               EventQueue.invokeAndWait(run);
+                       } catch (InterruptedException e) {
+                               error(e);
+                       } catch (InvocationTargetException e) {
+                               error(e);
+                       }
+               }
+       }
+
+       /**
+        * An error occurred, inform the user and/or log the error.
+        * 
+        * @param e
+        *            the error
+        */
+       static void error(Exception e) {
+               Instance.getTraceHandler().error(e);
+       }
+
+       /**
+        * An error occurred, inform the user and/or log the error.
+        * 
+        * @param e
+        *            the error message
+        */
+       static void error(String e) {
+               Instance.getTraceHandler().error(e);
+       }
+
+       /**
+        * Enables or disables this component, depending on the value of the
+        * parameter <code>b</code>. An enabled component can respond to user input
+        * and generate events. Components are enabled initially by default.
+        * <p>
+        * Disabling this component will also affect its children.
+        * 
+        * @param b
+        *            If <code>true</code>, this component is enabled; otherwise
+        *            this component is disabled
+        */
+       @Override
+       public void setEnabled(boolean b) {
+               super.setEnabled(b);
+               books.setEnabled(b);
+               searchPanel.setEnabled(b);
+       }
+
+       /**
+        * Set the item in wait mode, blocking it from accepting UI input.
+        * 
+        * @param waiting
+        *            TRUE for wait more, FALSE to restore normal mode
+        */
+       private void setWaiting(final boolean waiting) {
+               inUi(new Runnable() {
+                       @Override
+                       public void run() {
+                               GuiReaderSearchFrame.this.setEnabled(!waiting);
+                       }
+               });
+       }
+}
index b57bdc41fce373325a1a9a764fd77f2db18b33ab..bfb18921ecb5551aeb5eccd3782de4b7012eb2fa 100644 (file)
@@ -1,7 +1,6 @@
 package be.nikiroo.fanfix.reader.ui;
 
 import java.awt.BorderLayout;
-import java.awt.Color;
 import java.awt.Font;
 import java.awt.LayoutManager;
 import java.awt.event.ActionEvent;
@@ -9,7 +8,6 @@ import java.awt.event.ActionListener;
 
 import javax.swing.BorderFactory;
 import javax.swing.BoxLayout;
-import javax.swing.JButton;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
@@ -35,11 +33,9 @@ public class GuiReaderViewer extends JFrame {
        private Story story;
        private MetaData meta;
        private JLabel title;
-       private JLabel chapterLabel;
        private GuiReaderPropertiesPane descPane;
-       private int currentChapter = -42; // cover = -1
        private GuiReaderViewerPanel mainPanel;
-       private JButton[] navButtons;
+       private GuiReaderNavBar navbar;
 
        /**
         * Create a new {@link Story} viewer.
@@ -97,61 +93,46 @@ public class GuiReaderViewer extends JFrame {
         * initialise them.
         */
        private void initGuiNavButtons() {
-               JPanel navButtonsPane = new JPanel();
-               LayoutManager layout = new BoxLayout(navButtonsPane, BoxLayout.X_AXIS);
-               navButtonsPane.setLayout(layout);
-
-               navButtons = new JButton[4];
+               navbar = new GuiReaderNavBar(-1, story.getChapters().size() - 1) {
+                       private static final long serialVersionUID = 1L;
 
-               navButtons[0] = createNavButton("<<", new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               setChapter(-1);
-                       }
-               });
-               navButtons[1] = createNavButton(" < ", new ActionListener() {
                        @Override
-                       public void actionPerformed(ActionEvent e) {
-                               setChapter(currentChapter - 1);
-                       }
-               });
-               navButtons[2] = createNavButton(" > ", new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               setChapter(currentChapter + 1);
+                       protected String computeLabel(int index, int min, int max) {
+                               int chapter = index;
+                               Chapter chap;
+                               if (chapter < 0) {
+                                       chap = meta.getResume();
+                                       descPane.setVisible(true);
+                               } else {
+                                       chap = story.getChapters().get(chapter);
+                                       descPane.setVisible(false);
+                               }
+
+                               String chapterDisplay = GuiReader.trans(
+                                               StringIdGui.CHAPTER_HTML_UNNAMED, chap.getNumber(),
+                                               story.getChapters().size());
+                               if (chap.getName() != null && !chap.getName().trim().isEmpty()) {
+                                       chapterDisplay = GuiReader.trans(
+                                                       StringIdGui.CHAPTER_HTML_NAMED, chap.getNumber(),
+                                                       story.getChapters().size(), chap.getName());
+                               }
+
+                               return "<HTML>" + chapterDisplay + "</HTML>";
                        }
-               });
-               navButtons[3] = createNavButton(">>", new ActionListener() {
+               };
+
+               navbar.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               setChapter(story.getChapters().size() - 1);
+                               setChapter(navbar.getIndex());
                        }
                });
 
-               for (JButton navButton : navButtons) {
-                       navButtonsPane.add(navButton);
-               }
-
-               add(navButtonsPane, BorderLayout.SOUTH);
+               JPanel navButtonsPane = new JPanel();
+               LayoutManager layout = new BoxLayout(navButtonsPane, BoxLayout.X_AXIS);
+               navButtonsPane.setLayout(layout);
 
-               chapterLabel = new JLabel("");
-               navButtonsPane.add(chapterLabel);
-       }
-
-       /**
-        * Create a single navigation button.
-        * 
-        * @param text
-        *            the text to display
-        * @param action
-        *            the action to take on click
-        * @return the button
-        */
-       private JButton createNavButton(String text, ActionListener action) {
-               JButton navButton = new JButton(text);
-               navButton.addActionListener(action);
-               navButton.setForeground(Color.BLUE);
-               return navButton;
+               add(navbar, BorderLayout.SOUTH);
        }
 
        /**
@@ -163,36 +144,15 @@ public class GuiReaderViewer extends JFrame {
         *            the chapter number to set
         */
        private void setChapter(int chapter) {
-               navButtons[0].setEnabled(chapter >= 0);
-               navButtons[1].setEnabled(chapter >= 0);
-               navButtons[2].setEnabled(chapter + 1 < story.getChapters().size());
-               navButtons[3].setEnabled(chapter + 1 < story.getChapters().size());
-
-               if (chapter >= -1 && chapter < story.getChapters().size()
-                               && chapter != currentChapter) {
-                       currentChapter = chapter;
-
-                       Chapter chap;
-                       if (chapter == -1) {
-                               chap = meta.getResume();
-                               descPane.setVisible(true);
-                       } else {
-                               chap = story.getChapters().get(chapter);
-                               descPane.setVisible(false);
-                       }
-
-                       String chapterDisplay = GuiReader.trans(
-                                       StringIdGui.CHAPTER_HTML_UNNAMED, chap.getNumber(), story
-                                                       .getChapters().size());
-                       if (chap.getName() != null && !chap.getName().trim().isEmpty()) {
-                               chapterDisplay = GuiReader.trans(
-                                               StringIdGui.CHAPTER_HTML_NAMED, chap.getNumber(), story
-                                                               .getChapters().size(), chap.getName());
-                       }
-
-                       chapterLabel.setText("<HTML>" + chapterDisplay + "</HTML>");
-
-                       mainPanel.setChapter(chap);
+               Chapter chap;
+               if (chapter < 0) {
+                       chap = meta.getResume();
+                       descPane.setVisible(true);
+               } else {
+                       chap = story.getChapters().get(chapter);
+                       descPane.setVisible(false);
                }
+
+               mainPanel.setChapter(chap);
        }
 }
index 08a9c9c34e10901fcb5bce3644c9057b0eb991b4..724f552093942fdf42cb68347bdcaa4d71aa3aaf 100644 (file)
@@ -20,6 +20,7 @@ import javax.swing.SwingConstants;
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.bundles.StringIdGui;
 import be.nikiroo.fanfix.data.Chapter;
+import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.utils.Image;
 import be.nikiroo.utils.ui.ImageUtilsAwt;
@@ -51,12 +52,24 @@ public class GuiReaderViewerPanel extends JPanel {
         * Create a new viewer.
         * 
         * @param story
-        *            the {@link Story} to work on.
+        *            the {@link Story} to work on
         */
        public GuiReaderViewerPanel(Story story) {
+               this(story.getMeta(), story.getMeta().isImageDocument());
+       }
+
+       /**
+        * Create a new viewer.
+        * 
+        * @param meta
+        *            the {@link MetaData} of the story to show
+        * @param isImageDocument
+        *            TRUE if it is an image document, FALSE if not
+        */
+       public GuiReaderViewerPanel(MetaData meta, boolean isImageDocument) {
                super(new BorderLayout());
 
-               this.imageDocument = story.getMeta().isImageDocument();
+               this.imageDocument = isImageDocument;
 
                this.text = new JEditorPane("text/html", "");
                text.setEditable(false);
@@ -102,7 +115,7 @@ public class GuiReaderViewerPanel extends JPanel {
                        main.invalidate();
                }
 
-               setChapter(story.getMeta().getResume());
+               setChapter(meta.getResume());
        }
 
        /**
diff --git a/src/be/nikiroo/fanfix/searchable/BasicSearchable.java b/src/be/nikiroo/fanfix/searchable/BasicSearchable.java
new file mode 100644 (file)
index 0000000..d38505e
--- /dev/null
@@ -0,0 +1,276 @@
+package be.nikiroo.fanfix.searchable;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import org.jsoup.helper.DataUtil;
+import org.jsoup.nodes.Document;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.supported.BasicSupport;
+import be.nikiroo.fanfix.supported.SupportType;
+
+/**
+ * This class supports browsing through stories on the supported websites. It
+ * will fetch some {@link MetaData} that satisfy a search query or some tags if
+ * supported.
+ * 
+ * @author niki
+ */
+public abstract class BasicSearchable {
+       private SupportType type;
+       private BasicSupport support;
+
+       /**
+        * Create a new {@link BasicSearchable} of the given type.
+        * 
+        * @param type
+        *            the type, must not be NULL
+        */
+       public BasicSearchable(SupportType type) {
+               setType(type);
+               support = BasicSupport.getSupport(getType(), null);
+       }
+
+       /**
+        * Find the given tag by its hierarchical IDs.
+        * <p>
+        * I.E., it will take the tag A, subtag B, subsubtag C...
+        * 
+        * @param ids
+        *            the IDs to look for
+        * 
+        * @return the appropriate tag fully filled, or NULL if not found
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public SearchableTag getTag(Integer... ids) throws IOException {
+               SearchableTag tag = null;
+               List<SearchableTag> tags = getTags();
+
+               for (Integer tagIndex : ids) {
+                       // ! 1-based index !
+                       if (tagIndex == null || tags == null || tagIndex <= 0
+                                       || tagIndex > tags.size()) {
+                               return null;
+                       }
+
+                       tag = tags.get(tagIndex - 1);
+                       fillTag(tag);
+                       tags = tag.getChildren();
+               }
+
+               return tag;
+       }
+
+       /**
+        * The support type.
+        * 
+        * @return the type
+        */
+       public SupportType getType() {
+               return type;
+       }
+
+       /**
+        * The support type.
+        * 
+        * @param type
+        *            the new type
+        */
+       protected void setType(SupportType type) {
+               this.type = type;
+       }
+
+       /**
+        * The associated {@link BasicSupport}.
+        * <p>
+        * Mostly used to download content.
+        * 
+        * @return the support
+        */
+       protected BasicSupport getSupport() {
+               return support;
+       }
+
+       /**
+        * Get a list of tags that can be browsed here.
+        * 
+        * @return the list of tags
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       abstract public List<SearchableTag> getTags() throws IOException;
+
+       /**
+        * Fill the tag (set it 'complete') with more information from the support.
+        * 
+        * @param tag
+        *            the tag to fill
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       abstract public void fillTag(SearchableTag tag) throws IOException;
+
+       /**
+        * Search for the given term and return the number of pages of results of
+        * stories satisfying this search term.
+        * 
+        * @param search
+        *            the term to search for
+        * 
+        * @return a number of pages
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       abstract public int searchPages(String search) throws IOException;
+
+       /**
+        * Search for the given tag and return the number of pages of results of
+        * stories satisfying this tag.
+        * 
+        * @param tag
+        *            the tag to search for
+        * 
+        * @return a number of pages
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       abstract public int searchPages(SearchableTag tag) throws IOException;
+
+       /**
+        * Search for the given term and return a list of stories satisfying this
+        * search term.
+        * <p>
+        * Not that the returned stories will <b>NOT</b> be complete, but will only
+        * contain enough information to present them to the user and retrieve them.
+        * <p>
+        * URL is guaranteed to be usable, LUID will always be NULL.
+        * 
+        * @param search
+        *            the term to search for
+        * @param page
+        *            the page to use for result pagination, index is 1-based
+        * 
+        * @return a list of stories that satisfy that search term
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       abstract public List<MetaData> search(String search, int page)
+                       throws IOException;
+
+       /**
+        * Search for the given tag and return a list of stories satisfying this
+        * tag.
+        * <p>
+        * Not that the returned stories will <b>NOT</b> be complete, but will only
+        * contain enough information to present them to the user and retrieve them.
+        * <p>
+        * URL is guaranteed to be usable, LUID will always be NULL.
+        * 
+        * @param tag
+        *            the tag to search for
+        * @param page
+        *            the page to use for result pagination (see
+        *            {@link SearchableTag#getPages()}, remember to check for -1),
+        *            index is 1-based
+        * 
+        * @return a list of stories that satisfy that search term
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       abstract public List<MetaData> search(SearchableTag tag, int page)
+                       throws IOException;
+
+       /**
+        * Load a document from its url.
+        * 
+        * @param url
+        *            the URL to load
+        * @param stable
+        *            TRUE for more stable resources, FALSE when they often change
+        * 
+        * @return the document
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       protected Document load(String url, boolean stable) throws IOException {
+               return load(new URL(url), stable);
+       }
+
+       /**
+        * Load a document from its url.
+        * 
+        * @param url
+        *            the URL to load
+        * @param stable
+        *            TRUE for more stable resources, FALSE when they often change
+        * 
+        * @return the document
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       protected Document load(URL url, boolean stable) throws IOException {
+               return DataUtil.load(Instance.getCache().open(url, support, stable),
+                               "UTF-8", url.toString());
+       }
+
+       /**
+        * Return a {@link BasicSearchable} implementation supporting the given
+        * type, or NULL if it does not exist.
+        * 
+        * @param type
+        *            the type, can be NULL (will just return NULL, since we do not
+        *            support it)
+        * 
+        * @return an implementation that supports it, or NULL
+        */
+       static public BasicSearchable getSearchable(SupportType type) {
+               BasicSearchable support = null;
+
+               if (type != null) {
+                       switch (type) {
+                       case FIMFICTION:
+                               // TODO
+                               break;
+                       case FANFICTION:
+                               support = new Fanfiction(type);
+                               break;
+                       case MANGAFOX:
+                               // TODO
+                               break;
+                       case E621:
+                               // TODO
+                               break;
+                       case YIFFSTAR:
+                               // TODO
+                               break;
+                       case E_HENTAI:
+                               // TODO
+                               break;
+                       case MANGA_LEL:
+                               support = new MangaLel();
+                               break;
+                       case CBZ:
+                       case HTML:
+                       case INFO_TEXT:
+                       case TEXT:
+                       case EPUB:
+                               break;
+                       }
+               }
+
+               return support;
+       }
+}
diff --git a/src/be/nikiroo/fanfix/searchable/Fanfiction.java b/src/be/nikiroo/fanfix/searchable/Fanfiction.java
new file mode 100644 (file)
index 0000000..c2dfd5d
--- /dev/null
@@ -0,0 +1,415 @@
+package be.nikiroo.fanfix.searchable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringId;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.supported.SupportType;
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
+
+/**
+ * A {@link BasicSearchable} for Fanfiction.NET.
+ * 
+ * @author niki
+ */
+class Fanfiction extends BasicSearchable {
+       static private String BASE_URL = "http://fanfiction.net/";
+
+       /**
+        * Create a new {@link Fanfiction}.
+        * 
+        * @param type
+        *            {@link SupportType#FANFICTION}
+        */
+       public Fanfiction(SupportType type) {
+               super(type);
+       }
+
+       @Override
+       public List<SearchableTag> getTags() throws IOException {
+               String storiesName = null;
+               String crossoversName = null;
+               Map<String, String> stories = new HashMap<String, String>();
+               Map<String, String> crossovers = new HashMap<String, String>();
+
+               Document mainPage = load(BASE_URL, true);
+               Element menu = mainPage.getElementsByClass("dropdown").first();
+               if (menu != null) {
+                       Element ul = menu.getElementsByClass("dropdown-menu").first();
+                       if (ul != null) {
+                               Map<String, String> currentList = null;
+                               for (Element li : ul.getElementsByTag("li")) {
+                                       if (li.hasClass("disabled")) {
+                                               if (storiesName == null) {
+                                                       storiesName = li.text();
+                                                       currentList = stories;
+                                               } else {
+                                                       crossoversName = li.text();
+                                                       currentList = crossovers;
+                                               }
+                                       } else if (currentList != null) {
+                                               Element a = li.getElementsByTag("a").first();
+                                               if (a != null) {
+                                                       currentList.put(a.absUrl("href"), a.text());
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               List<SearchableTag> tags = new ArrayList<SearchableTag>();
+
+               if (storiesName != null) {
+                       SearchableTag tag = new SearchableTag(null, storiesName, false);
+                       for (String id : stories.keySet()) {
+                               tag.add(new SearchableTag(id, stories.get(id), false, false));
+                       }
+                       tags.add(tag);
+               }
+
+               if (crossoversName != null) {
+                       SearchableTag tag = new SearchableTag(null, crossoversName, false);
+                       for (String id : crossovers.keySet()) {
+                               tag.add(new SearchableTag(id, crossovers.get(id), false, false));
+                       }
+                       tags.add(tag);
+               }
+
+               return tags;
+       }
+
+       @Override
+       public void fillTag(SearchableTag tag) throws IOException {
+               if (tag.getId() == null || tag.isComplete()) {
+                       return;
+               }
+
+               Document doc = load(tag.getId(), false);
+               Element list = doc.getElementById("list_output");
+               if (list != null) {
+                       Element table = list.getElementsByTag("table").first();
+                       if (table != null) {
+                               for (Element div : table.getElementsByTag("div")) {
+                                       Element a = div.getElementsByTag("a").first();
+                                       Element span = div.getElementsByTag("span").first();
+
+                                       if (a != null) {
+                                               String subid = a.absUrl("href");
+                                               boolean crossoverSubtag = subid
+                                                               .contains("/crossovers/");
+
+                                               SearchableTag subtag = new SearchableTag(subid,
+                                                               a.text(), !crossoverSubtag, !crossoverSubtag);
+
+                                               tag.add(subtag);
+                                               if (span != null) {
+                                                       String nr = span.text();
+                                                       if (nr.startsWith("(")) {
+                                                               nr = nr.substring(1);
+                                                       }
+                                                       if (nr.endsWith(")")) {
+                                                               nr = nr.substring(0, nr.length() - 1);
+                                                       }
+                                                       nr = nr.trim();
+
+                                                       // TODO: fix toNumber/fromNumber
+                                                       nr = nr.replaceAll("\\.[0-9]*", "");
+
+                                                       subtag.setCount(StringUtils.toNumber(nr));
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               tag.setComplete(true);
+       }
+
+       @Override
+       public List<MetaData> search(String search, int page) throws IOException {
+               String encoded = URLEncoder.encode(search.toLowerCase(), "utf-8");
+               String url = BASE_URL + "search/?ready=1&type=story&keywords="
+                               + encoded + "&ppage=" + page;
+
+               return getStories(url, null, null);
+       }
+
+       @Override
+       public List<MetaData> search(SearchableTag tag, int page)
+                       throws IOException {
+               List<MetaData> metas = new ArrayList<MetaData>();
+
+               String url = tag.getId();
+               if (url != null) {
+                       if (page > 1) {
+                               int pos = url.indexOf("&p=");
+                               if (pos >= 0) {
+                                       url = url.replaceAll("(.*\\&p=)[0-9]*(.*)", "$1\\" + page
+                                                       + "$2");
+                               } else {
+                                       url += "&p=" + page;
+                               }
+                       }
+
+                       Document doc = load(url, false);
+
+                       // Update the pages number if needed
+                       if (tag.getPages() < 0 && tag.isLeaf()) {
+                               tag.setPages(getPages(doc));
+                       }
+
+                       // Find out the full subjects (including parents)
+                       String subjects = "";
+                       for (SearchableTag t = tag; t != null; t = t.getParent()) {
+                               if (!subjects.isEmpty()) {
+                                       subjects += ", ";
+                               }
+                               subjects += t.getName();
+                       }
+
+                       metas = getStories(url, doc, subjects);
+               }
+
+               return metas;
+       }
+
+       @Override
+       public int searchPages(String search) throws IOException {
+               String encoded = URLEncoder.encode(search.toLowerCase(), "utf-8");
+               String url = BASE_URL + "search/?ready=1&type=story&keywords="
+                               + encoded;
+
+               return getPages(load(url, false));
+       }
+
+       @Override
+       public int searchPages(SearchableTag tag) throws IOException {
+               if (tag.isLeaf()) {
+                       String url = tag.getId();
+                       return getPages(load(url, false));
+               }
+
+               return 0;
+       }
+
+       /**
+        * Return the number of pages in this stories result listing.
+        * 
+        * @param doc
+        *            the document
+        * 
+        * @return the number of pages or -1 if unknown
+        */
+       private int getPages(Document doc) {
+               int pages = -1;
+
+               if (doc != null) {
+                       Element center = doc.getElementsByTag("center").first();
+                       if (center != null) {
+                               for (Element a : center.getElementsByTag("a")) {
+                                       if (a.absUrl("href").contains("&p=")) {
+                                               int thisLinkPages = -1;
+                                               try {
+                                                       String[] tab = a.absUrl("href").split("=");
+                                                       tab = tab[tab.length - 1].split("&");
+                                                       thisLinkPages = Integer
+                                                                       .parseInt(tab[tab.length - 1]);
+                                               } catch (Exception e) {
+                                               }
+
+                                               pages = Math.max(pages, thisLinkPages);
+                                       }
+                               }
+                       }
+               }
+
+               return pages;
+       }
+
+       /**
+        * Fetch the stories from the given page.
+        * 
+        * @param sourceUrl
+        *            the url of the document
+        * @param doc
+        *            the document to use (if NULL, will be loaded from
+        *            <tt>sourceUrl</tt>)
+        * @param mainSubject
+        *            the main subject (the anime/book/movie item related to the
+        *            stories, like "MLP" or "Doctor Who"), or NULL if none
+        * 
+        * @return the stories found in it
+        * 
+        * @throws IOException
+        *             in case of I/O errors
+        */
+       private List<MetaData> getStories(String sourceUrl, Document doc,
+                       String mainSubject) throws IOException {
+               List<MetaData> metas = new ArrayList<MetaData>();
+
+               if (doc == null) {
+                       doc = load(sourceUrl, false);
+               }
+
+               for (Element story : doc.getElementsByClass("z-list")) {
+                       MetaData meta = new MetaData();
+                       meta.setImageDocument(false);
+                       meta.setSource(getType().getSourceName());
+                       meta.setPublisher(getType().getSourceName());
+                       meta.setType(getType().toString());
+
+                       // Title, URL, Cover
+                       Element stitle = story.getElementsByClass("stitle").first();
+                       if (stitle != null) {
+                               meta.setTitle(stitle.text());
+                               meta.setUrl(stitle.absUrl("href"));
+                               meta.setUuid(meta.getUrl());
+                               Element cover = stitle.getElementsByTag("img").first();
+                               if (cover != null) {
+                                       // note: see data-original if needed?
+                                       String coverUrl = cover.absUrl("src");
+
+                                       try {
+                                               InputStream in = Instance.getCache().open(
+                                                               new URL(coverUrl), getSupport(), true);
+                                               try {
+                                                       meta.setCover(new Image(in));
+                                               } finally {
+                                                       in.close();
+                                               }
+                                       } catch (Exception e) {
+                                               // Should not happen on Fanfiction.net
+                                               Instance.getTraceHandler().error(
+                                                               new Exception(
+                                                                               "Cannot download cover for Fanfiction story in search mode: "
+                                                                                               + meta.getTitle(), e));
+                                       }
+                               }
+                       }
+
+                       // Author
+                       Elements as = story.getElementsByTag("a");
+                       if (as.size() > 1) {
+                               meta.setAuthor(as.get(1).text());
+                       }
+
+                       // Tags (concatenated text), published date, updated date, Resume
+                       String tags = "";
+                       List<String> tagList = new ArrayList<String>();
+                       Elements divs = story.getElementsByTag("div");
+                       if (divs.size() > 1 && divs.get(1).childNodeSize() > 0) {
+                               String resume = divs.get(1).text();
+                               if (divs.size() > 2) {
+                                       tags = divs.get(2).text();
+                                       resume = resume.substring(0,
+                                                       resume.length() - tags.length()).trim();
+
+                                       for (Element d : divs.get(2).getElementsByAttribute(
+                                                       "data-xutime")) {
+                                               String secs = d.attr("data-xutime");
+                                               try {
+                                                       String date = new SimpleDateFormat("yyyy-MM-dd")
+                                                                       .format(new Date(
+                                                                                       Long.parseLong(secs) * 1000));
+                                                       // (updated, ) published
+                                                       if (meta.getDate() != null) {
+                                                               tagList.add("Updated: " + meta.getDate());
+                                                       }
+                                                       meta.setDate(date);
+                                               } catch (Exception e) {
+                                               }
+                                       }
+                               }
+
+                               meta.setResume(getSupport().makeChapter(new URL(sourceUrl), 0,
+                                               Instance.getTrans().getString(StringId.DESCRIPTION),
+                                               resume));
+                       }
+
+                       // How are the tags ordered?
+                       // We have "Rated: xx", then the language, then all other tags
+                       // If the subject(s) is/are present, they are before "Rated: xx"
+
+                       // ////////////
+                       // Examples: //
+                       // ////////////
+
+                       // Search (Luna) Tags: [Harry Potter, Rated: T, English, Chapters:
+                       // 1, Words: 270, Reviews: 2, Published: 2/19/2013, Luna L.]
+
+                       // Normal (MLP) Tags: [Rated: T, Spanish, Drama/Suspense, Chapters:
+                       // 2, Words: 8,686, Reviews: 1, Favs: 1, Follows: 1, Updated: 4/7,
+                       // Published: 4/2]
+
+                       // Crossover (MLP/Who) Tags: [Rated: K+, English, Adventure/Romance,
+                       // Chapters: 8, Words: 7,788, Reviews: 2, Favs: 2, Follows: 1,
+                       // Published: 9/1/2016]
+
+                       boolean rated = false;
+                       boolean isLang = false;
+                       String subject = mainSubject == null ? "" : mainSubject;
+                       String[] tab = tags.split("  *-  *");
+                       for (int i = 0; i < tab.length; i++) {
+                               String tag = tab[i];
+                               if (tag.startsWith("Rated: ")) {
+                                       rated = true;
+                               }
+
+                               if (!rated) {
+                                       if (!subject.isEmpty()) {
+                                               subject += ", ";
+                                       }
+                                       subject += tag;
+                               } else if (isLang) {
+                                       meta.setLang(tag);
+                                       isLang = false;
+                               } else {
+                                       if (tag.contains(":")) {
+                                               // Handle special tags:
+                                               if (tag.startsWith("Words: ")) {
+                                                       try {
+                                                               meta.setWords(Long.parseLong(tag
+                                                                               .substring("Words: ".length())
+                                                                               .replace(",", "").trim()));
+                                                       } catch (Exception e) {
+                                                       }
+                                               } else if (tag.startsWith("Rated: ")) {
+                                                       tagList.add(tag);
+                                               }
+                                       } else {
+                                               // Normal tags are "/"-separated
+                                               for (String t : tag.split("/")) {
+                                                       tagList.add(t);
+                                               }
+                                       }
+
+                                       if (tag.startsWith("Rated: ")) {
+                                               isLang = true;
+                                       }
+                               }
+                       }
+
+                       meta.setSubject(subject);
+                       meta.setTags(tagList);
+
+                       metas.add(meta);
+               }
+
+               return metas;
+       }
+}
diff --git a/src/be/nikiroo/fanfix/searchable/MangaLel.java b/src/be/nikiroo/fanfix/searchable/MangaLel.java
new file mode 100644 (file)
index 0000000..3e2924f
--- /dev/null
@@ -0,0 +1,192 @@
+package be.nikiroo.fanfix.searchable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.jsoup.helper.DataUtil;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringId;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.supported.SupportType;
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
+
+class MangaLel extends BasicSearchable {
+       private String BASE_URL = "http://mangas-lecture-en-ligne.fr/index_lel.php";
+
+       public MangaLel() {
+               super(SupportType.MANGA_LEL);
+       }
+
+       @Override
+       public List<SearchableTag> getTags() throws IOException {
+               List<SearchableTag> tags = new ArrayList<SearchableTag>();
+
+               String url = BASE_URL + "?page=recherche";
+               Document doc = load(url, false);
+
+               Element genre = doc.getElementsByClass("genre").first();
+               if (genre != null) {
+                       for (Element el : genre.getElementsByAttributeValueStarting("for",
+                                       "genre")) {
+                               tags.add(new SearchableTag(el.attr("for"), el.text(), true));
+                       }
+               }
+
+               return tags;
+       }
+
+       @Override
+       public void fillTag(SearchableTag tag) throws IOException {
+               // Tags are always complete
+       }
+
+       @Override
+       public List<MetaData> search(String search, int page) throws IOException {
+               String url = BASE_URL + "?nomProjet="
+                               + URLEncoder.encode(search, "utf-8")
+                               + "&nomAuteur=&nomTeam=&page=recherche&truc=truc";
+
+               // No pagination
+               return getResults(url);
+       }
+
+       @Override
+       public List<MetaData> search(SearchableTag tag, int page)
+                       throws IOException {
+               String url = BASE_URL + "?nomProjet=&nomAuteur=&nomTeam=&"
+                               + tag.getId() + "=on&page=recherche&truc=truc";
+
+               // No pagination
+               return getResults(url);
+       }
+
+       @Override
+       public int searchPages(String search) throws IOException {
+               // No pagination
+               return 1;
+       }
+
+       @Override
+       public int searchPages(SearchableTag tag) throws IOException {
+               if (tag.isLeaf()) {
+                       // No pagination
+                       return 1;
+               }
+
+               return 0;
+       }
+
+       private List<MetaData> getResults(String sourceUrl) throws IOException {
+               List<MetaData> metas = new ArrayList<MetaData>();
+
+               Document doc = DataUtil.load(
+                               Instance.getCache().open(new URL(sourceUrl), getSupport(),
+                                               false), "UTF-8", sourceUrl);
+
+               for (Element result : doc.getElementsByClass("rechercheAffichage")) {
+                       Element a = result.getElementsByTag("a").first();
+                       if (a != null) {
+                               int projectId = -1;
+
+                               MetaData meta = new MetaData();
+
+                               // Target:
+                               // http://mangas-lecture-en-ligne.fr/index_lel.php?page=presentationProjet&idProjet=218
+
+                               // a.absUrl("href"):
+                               // http://mangas-lecture-en-ligne.fr/index_lel?onCommence=oui&idChapitre=2805
+
+                               // ...but we need the PROJECT id, not the CHAPTER id -> use
+                               // <IMG>
+
+                               Elements infos = result.getElementsByClass("texte");
+                               if (infos != null) {
+                                       String[] tab = infos.outerHtml().split("<br>");
+
+                                       meta.setLang("fr");
+                                       meta.setSource(getType().getSourceName());
+                                       meta.setPublisher(getType().getSourceName());
+                                       meta.setType(getType().toString());
+                                       meta.setSubject("manga");
+                                       meta.setImageDocument(true);
+                                       meta.setTitle(getVal(tab, 0));
+                                       meta.setAuthor(getVal(tab, 1));
+                                       meta.setTags(Arrays.asList(getVal(tab, 2).split(" ")));
+
+                                       meta.setResume(getSupport()
+                                                       .makeChapter(
+                                                                       new URL(sourceUrl),
+                                                                       0,
+                                                                       Instance.getTrans().getString(
+                                                                                       StringId.DESCRIPTION),
+                                                                       getVal(tab, 5)));
+                               }
+
+                               Element img = result.getElementsByTag("img").first();
+                               if (img != null) {
+                                       try {
+                                               String[] tab = img.attr("src").split("/");
+                                               String str = tab[tab.length - 1];
+                                               tab = str.split("\\.");
+                                               str = tab[0];
+                                               projectId = Integer.parseInt(str);
+
+                                               String coverUrl = img.absUrl("src");
+                                               try {
+                                                       InputStream in = Instance.getCache().open(
+                                                                       new URL(coverUrl), getSupport(), true);
+                                                       try {
+                                                               meta.setCover(new Image(in));
+                                                       } finally {
+                                                               in.close();
+                                                       }
+                                               } catch (Exception e) {
+                                                       // Happen often on MangaLEL...
+                                                       Instance.getTraceHandler().trace(
+                                                                       "Cannot download cover for MangaLEL story in search mode: "
+                                                                                       + meta.getTitle());
+                                               }
+                                       } catch (Exception e) {
+                                               // no project id... cannot use the story :(
+                                               Instance.getTraceHandler().error(
+                                                               "Cannot find ProjectId for MangaLEL story in search mode: "
+                                                                               + meta.getTitle());
+                                       }
+                               }
+
+                               if (projectId >= 0) {
+                                       meta.setUrl("http://mangas-lecture-en-ligne.fr/index_lel.php?page=presentationProjet&idProjet="
+                                                       + projectId);
+                                       meta.setUuid(meta.getUrl());
+                                       metas.add(meta);
+                               }
+                       }
+               }
+
+               return metas;
+       }
+
+       private String getVal(String[] tab, int i) {
+               String val = "";
+
+               if (i < tab.length) {
+                       val = StringUtils.unhtml(tab[i]);
+                       int pos = val.indexOf(":");
+                       if (pos >= 0) {
+                               val = val.substring(pos + 1).trim();
+                       }
+               }
+
+               return val;
+       }
+}
diff --git a/src/be/nikiroo/fanfix/searchable/SearchableTag.java b/src/be/nikiroo/fanfix/searchable/SearchableTag.java
new file mode 100644 (file)
index 0000000..de86798
--- /dev/null
@@ -0,0 +1,324 @@
+package be.nikiroo.fanfix.searchable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a tag that can be searched on a supported website.
+ * 
+ * @author niki
+ */
+public class SearchableTag {
+       private String id;
+       private String name;
+       private boolean complete;
+       private long count;
+
+       private SearchableTag parent;
+       private List<SearchableTag> children;
+
+       /**
+        * The number of stories result pages this tag can get.
+        * <p>
+        * We keep more information than what the getter/setter returns/accepts.
+        * <ul>
+        * <li>-2: this tag does not support stories results (not a leaf tag)</li>
+        * <li>-1: the number is not yet known, but will be known after a
+        * {@link BasicSearchable#fillTag(SearchableTag)} operation</li>
+        * <li>X: the number of pages</li>
+        * </ul>
+        */
+       private int pages;
+
+       /**
+        * Create a new {@link SearchableTag}.
+        * <p>
+        * Note that tags are complete by default.
+        * 
+        * @param id
+        *            the ID (usually a way to find the linked stories later on)
+        * @param name
+        *            the tag name, which can be displayed to the user
+        * @param leaf
+        *            the tag is a leaf tag, that is, it will not return subtags
+        *            with {@link BasicSearchable#fillTag(SearchableTag)} but will
+        *            return stories with
+        *            {@link BasicSearchable#search(SearchableTag, int)}
+        */
+       public SearchableTag(String id, String name, boolean leaf) {
+               this(id, name, leaf, true);
+       }
+
+       /**
+        * Create a new {@link SearchableTag}.
+        * 
+        * @param id
+        *            the ID (usually a way to find the linked stories later on)
+        * @param name
+        *            the tag name, which can be displayed to the user
+        * @param leaf
+        *            the tag is a leaf tag, that is, it will not return subtags
+        *            with {@link BasicSearchable#fillTag(SearchableTag)} but will
+        *            return stories with
+        *            {@link BasicSearchable#search(SearchableTag, int)}
+        * @param complete
+        *            the tag {@link SearchableTag#isComplete()} or not
+        */
+       public SearchableTag(String id, String name, boolean leaf, boolean complete) {
+               this.id = id;
+               this.name = name;
+               this.complete = leaf || complete;
+
+               setLeaf(leaf);
+
+               children = new ArrayList<SearchableTag>();
+       }
+
+       /**
+        * The ID (usually a way to find the linked stories later on).
+        * 
+        * @return the ID
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * The tag name, which can be displayed to the user.
+        * 
+        * @return then name
+        */
+       public String getName() {
+               return name;
+       }
+
+       /**
+        * The fully qualified tag name, which can be displayed to the user.
+        * <p>
+        * It will display all the tags that lead to this one as well as this one.
+        * 
+        * @return the fully qualified name
+        */
+       public String getFqName() {
+               if (parent != null) {
+                       return parent.getFqName() + " / " + name;
+               }
+
+               return "" + name;
+       }
+
+       /**
+        * Non-complete, non-leaf tags can still be completed via a
+        * {@link BasicSearchable#fillTag(SearchableTag)} operation from a
+        * {@link BasicSearchable}, in order to gain (more?) subtag children.
+        * <p>
+        * Leaf tags are always considered complete.
+        * 
+        * @return TRUE if it is complete
+        */
+       public boolean isComplete() {
+               return complete;
+       }
+
+       /**
+        * Non-complete, non-leaf tags can still be completed via a
+        * {@link BasicSearchable#fillTag(SearchableTag)} operation from a
+        * {@link BasicSearchable}, in order to gain (more?) subtag children.
+        * <p>
+        * Leaf tags are always considered complete.
+        * 
+        * @param complete
+        *            TRUE if it is complete
+        */
+       public void setComplete(boolean complete) {
+               this.complete = isLeaf() || complete;
+       }
+
+       /**
+        * The number of items that can be found with this tag if it is searched.
+        * <p>
+        * Will report the number of subtags by default.
+        * 
+        * @return the number of items
+        */
+       public long getCount() {
+               long count = this.count;
+               if (count <= 0) {
+                       count = children.size();
+               }
+
+               return count;
+       }
+
+       /**
+        * The number of items that can be found with this tag if it is searched.
+        * 
+        * @param count
+        *            the new count
+        */
+       public void setCount(long count) {
+               this.count = count;
+       }
+
+       /**
+        * The number of stories result pages this tag contains, only make sense if
+        * {@link SearchableTag#isLeaf()} returns TRUE.
+        * <p>
+        * Will return -1 if the number is not yet known.
+        * 
+        * @return the number of pages, or -1
+        */
+       public int getPages() {
+               return Math.max(-1, pages);
+       }
+
+       /**
+        * The number of stories result pages this tag contains, only make sense if
+        * {@link SearchableTag#isLeaf()} returns TRUE.
+        * 
+        * @param pages
+        *            the (positive or 0) number of pages
+        */
+       public void setPages(int pages) {
+               this.pages = Math.max(-1, pages);
+       }
+
+       /**
+        * This tag is a leaf tag, that is, it will not return other subtags with
+        * {@link BasicSearchable#fillTag(SearchableTag)} but will return stories
+        * with {@link BasicSearchable#search(SearchableTag, int)}.
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isLeaf() {
+               return pages > -2;
+       }
+
+       /**
+        * This tag is a leaf tag, that is, it will not return other subtags with
+        * {@link BasicSearchable#fillTag(SearchableTag)} but will return stories
+        * with {@link BasicSearchable#search(SearchableTag, int)}.
+        * <p>
+        * Will reset the number of pages to -1.
+        * 
+        * @param leaf
+        *            TRUE if it is
+        */
+       public void setLeaf(boolean leaf) {
+               pages = leaf ? -1 : -2;
+               if (leaf) {
+                       complete = true;
+               }
+       }
+
+       /**
+        * The subtag children of this {@link SearchableTag}.
+        * <p>
+        * Never NULL.
+        * <p>
+        * Note that if {@link SearchableTag#isComplete()} returns false, you can
+        * still fill (more?) subtag children with a {@link BasicSearchable}.
+        * 
+        * @return the subtag children, never NULL
+        */
+       public List<SearchableTag> getChildren() {
+               return children;
+       }
+
+       /**
+        * Add the given {@link SearchableTag} as a subtag child.
+        * 
+        * @param tag
+        *            the tag to add
+        */
+       public void add(SearchableTag tag) {
+               if (tag == null) {
+                       throw new NullPointerException("tag");
+               }
+
+               for (SearchableTag p = this; p != null; p = p.parent) {
+                       if (p.equals(tag)) {
+                               throw new IllegalArgumentException(
+                                               "Tags do not allow recursion");
+                       }
+               }
+               for (SearchableTag p = tag; p != null; p = p.parent) {
+                       if (p.equals(this)) {
+                               throw new IllegalArgumentException(
+                                               "Tags do not allow recursion");
+                       }
+               }
+
+               children.add(tag);
+               tag.parent = this;
+       }
+
+       /**
+        * This {@link SearchableTag} parent tag, or NULL if none.
+        * 
+        * @return the parent or NULL
+        */
+       public SearchableTag getParent() {
+               return parent;
+       }
+
+       /**
+        * Display a DEBUG {@link String} representation of this object.
+        */
+       @Override
+       public String toString() {
+               String rep = name + " [" + id + "]";
+               if (!complete) {
+                       rep += "*";
+               }
+
+               if (getCount() > 0) {
+                       rep += " (" + getCount() + ")";
+               }
+
+               if (!children.isEmpty()) {
+                       String tags = "";
+                       int i = 1;
+                       for (SearchableTag tag : children) {
+                               if (!tags.isEmpty()) {
+                                       tags += ", ";
+                               }
+
+                               if (i > 10) {
+                                       tags += "...";
+                                       break;
+                               }
+
+                               tags += tag;
+                               i++;
+                       }
+
+                       rep += ": " + tags;
+               }
+
+               return rep;
+       }
+
+       @Override
+       public int hashCode() {
+               return getFqName().hashCode();
+       }
+
+       @Override
+       public boolean equals(Object otherObj) {
+               if (otherObj instanceof SearchableTag) {
+                       SearchableTag other = (SearchableTag) otherObj;
+                       if ((id == null && other.id == null)
+                                       || (id != null && id.equals(other.id))) {
+                               if (getFqName().equals(other.getFqName())) {
+                                       if ((parent == null && other.parent == null)
+                                                       || (parent != null && parent.equals(other.parent))) {
+                                               return true;
+                                       }
+                               }
+                       }
+               }
+
+               return false;
+       }
+}
index 4337626c7fe60d96914eb9658df2b827823de539..092f89e2a35bba003761046e483ed7f5766a8844 100644 (file)
@@ -38,13 +38,6 @@ public abstract class BasicSupport {
        private SupportType type;
        private URL currentReferer; // with only one 'r', as in 'HTTP'...
 
-       /**
-        * The name of this support class.
-        * 
-        * @return the name
-        */
-       protected abstract String getSourceName();
-
        /**
         * Check if the given resource is supported by this {@link BasicSupport}.
         * 
@@ -311,7 +304,7 @@ public abstract class BasicSupport {
         * @throws IOException
         *             in case of I/O error
         */
-       // ADD final when BasicSupport_Deprecated is gone
+       // TODO: ADD final when BasicSupport_Deprecated is gone
        public Story process(Progress pg) throws IOException {
                setCurrentReferer(source);
                login();
@@ -402,6 +395,29 @@ public abstract class BasicSupport {
                return story;
        }
 
+       /**
+        * Create a chapter from the given data.
+        * 
+        * @param source
+        *            the source URL for this content, which can be used to try and
+        *            find images if images are present in the format [image-url]
+        * @param number
+        *            the chapter number (0 = description)
+        * @param name
+        *            the chapter name
+        * @param content
+        *            the content of the chapter
+        * @return the {@link Chapter}
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public Chapter makeChapter(URL source, int number, String name,
+                       String content) throws IOException {
+               return BasicSupportPara.makeChapter(this, source, number, name,
+                               content, isHtml(), null);
+       }
+
        /**
         * Return a {@link BasicSupport} implementation supporting the given
         * resource if possible.
@@ -441,10 +457,11 @@ public abstract class BasicSupport {
         * Return a {@link BasicSupport} implementation supporting the given type.
         * 
         * @param type
-        *            the type
+        *            the type, must not be NULL
         * @param url
         *            the {@link URL} to support (can be NULL to get an
-        *            "abstract support")
+        *            "abstract support"; if not NULL, will be used as the source
+        *            URL)
         * 
         * @return an implementation that supports it, or NULL
         */
index f8ea9d469570cfd4a1a63148b6167bfdf028ac44..d1dbc00b4595d3517f20f4cd0a50dc449b4cfc36 100644 (file)
@@ -681,7 +681,6 @@ public abstract class BasicSupport_Deprecated extends BasicSupport {
                        // try for files
                        if (source != null) {
                                try {
-
                                        String relPath = null;
                                        String absPath = null;
                                        try {
index 062adf0fac71550a8946257f9123e2d178ba7d98..3682afe520706748c36e1132cf56770d02c919bb 100644 (file)
@@ -1,7 +1,6 @@
 package be.nikiroo.fanfix.supported;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
@@ -21,8 +20,8 @@ import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
-import be.nikiroo.utils.MarkableFileInputStream;
 import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.streams.MarkableFileInputStream;
 
 /**
  * Support class for CBZ files (works better with CBZ created with this program,
@@ -36,11 +35,6 @@ class Cbz extends Epub {
                return url.toString().toLowerCase().endsWith(".cbz");
        }
 
-       @Override
-       public String getSourceName() {
-               return "cbz";
-       }
-
        @Override
        protected String getDataPrefix() {
                return "";
@@ -83,8 +77,7 @@ class Cbz extends Epub {
                InputStream cbzIn = null;
                ZipInputStream zipIn = null;
                try {
-                       cbzIn = new MarkableFileInputStream(new FileInputStream(
-                                       getSourceFileOriginal()));
+                       cbzIn = new MarkableFileInputStream(getSourceFileOriginal());
                        zipIn = new ZipInputStream(cbzIn);
                        for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn
                                        .getNextEntry()) {
@@ -138,25 +131,27 @@ class Cbz extends Epub {
                        story.setChapters(new ArrayList<Chapter>());
 
                        // Check if we can find non-images chapters, for hybrid-cbz support
-                       for (Chapter chap : origStory) {
-                               Boolean isImages = null;
-                               for (Paragraph para : chap) {
-                                       ParagraphType t = para.getType();
-                                       if (isImages == null && !t.isText(true)) {
-                                               isImages = true;
-                                       }
-                                       if (t.isText(false)) {
-                                               String line = para.getContent();
-                                               // Images are saved in text mode as "[image-link]"
-                                               if (!(line.startsWith("[") && line.endsWith("]"))) {
-                                                       isImages = false;
+                       if (origStory != null) {
+                               for (Chapter chap : origStory) {
+                                       Boolean isImages = null;
+                                       for (Paragraph para : chap) {
+                                               ParagraphType t = para.getType();
+                                               if (isImages == null && !t.isText(true)) {
+                                                       isImages = true;
+                                               }
+                                               if (t.isText(false)) {
+                                                       String line = para.getContent();
+                                                       // Images are saved in text mode as "[image-link]"
+                                                       if (!(line.startsWith("[") && line.endsWith("]"))) {
+                                                               isImages = false;
+                                                       }
                                                }
                                        }
-                               }
 
-                               if (isImages != null && !isImages) {
-                                       story.getChapters().add(chap);
-                                       chap.setNumber(story.getChapters().size());
+                                       if (isImages != null && !isImages) {
+                                               story.getChapters().add(chap);
+                                               chap.setNumber(story.getChapters().size());
+                                       }
                                }
                        }
 
index 36b9dad3c9bf3520053e938539a9db149e134695..dfa9e5ed6a60e4694fc8494dcf196b4328259ee6 100644 (file)
@@ -33,11 +33,6 @@ import be.nikiroo.utils.StringUtils;
  * @author niki
  */
 class E621 extends BasicSupport_Deprecated {
-       @Override
-       public String getSourceName() {
-               return "e621.net";
-       }
-
        @Override
        protected MetaData getMeta(URL source, InputStream in) throws IOException {
                MetaData meta = new MetaData();
@@ -46,9 +41,9 @@ class E621 extends BasicSupport_Deprecated {
                meta.setAuthor(getAuthor(source, reset(in)));
                meta.setDate("");
                meta.setTags(getTags(source, reset(in), false));
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en");
index 9ed4e89e60996c6683876e543803ca55581117ad..67585cd477f90a257acd94c9d28c67bac07575f7 100644 (file)
@@ -26,11 +26,6 @@ import be.nikiroo.utils.StringUtils;
  * @author niki
  */
 class EHentai extends BasicSupport_Deprecated {
-       @Override
-       public String getSourceName() {
-               return "e-hentai.org";
-       }
-
        @Override
        protected MetaData getMeta(URL source, InputStream in) throws IOException {
                MetaData meta = new MetaData();
@@ -39,9 +34,9 @@ class EHentai extends BasicSupport_Deprecated {
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate(getDate(reset(in)));
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang(getLang(reset(in)));
index e5261d36b7a9e291ebfd16b303f22cf51d9b35db..ae26574340c58cc9e19d54889f70fb77dadf272a 100644 (file)
@@ -17,7 +17,7 @@ import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
-import be.nikiroo.utils.MarkableFileInputStream;
+import be.nikiroo.utils.streams.MarkableFileInputStream;
 import be.nikiroo.utils.StringUtils;
 
 /**
@@ -34,11 +34,6 @@ class Epub extends InfoText {
        private URL fakeSource;
        private InputStream fakeIn;
 
-       @Override
-       public String getSourceName() {
-               return "epub";
-       }
-
        public File getSourceFileOriginal() {
                return super.getSourceFile();
        }
@@ -179,8 +174,7 @@ class Epub extends InfoText {
                        }
 
                        if (tmp.exists()) {
-                               this.fakeIn = new MarkableFileInputStream(new FileInputStream(
-                                               tmp));
+                               this.fakeIn = new MarkableFileInputStream(tmp);
                        }
 
                        if (tmpInfo.exists()) {
@@ -198,7 +192,7 @@ class Epub extends InfoText {
                                meta = new MetaData();
                                meta.setLang("en");
                                meta.setTags(new ArrayList<String>());
-                               meta.setSource(getSourceName());
+                               meta.setSource(getType().getSourceName());
                                meta.setUuid(url);
                                meta.setUrl(url);
                                meta.setTitle(title);
index 9b749bc6753e1efc92c2ffb48d1d6a83f1981b1e..64df8d3195bb6274668f26231e459701c11eac72 100644 (file)
@@ -32,11 +32,6 @@ class Fanfiction extends BasicSupport_Deprecated {
                return true;
        }
 
-       @Override
-       public String getSourceName() {
-               return "Fanfiction.net";
-       }
-
        @Override
        protected MetaData getMeta(URL source, InputStream in) throws IOException {
                MetaData meta = new MetaData();
@@ -45,9 +40,9 @@ class Fanfiction extends BasicSupport_Deprecated {
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate(getDate(reset(in)));
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en"); // TODO!
@@ -123,7 +118,7 @@ class Fanfiction extends BasicSupport_Deprecated {
                        }
                }
 
-               return null;
+               return "";
        }
 
        private String getAuthor(InputStream in) {
index 792f66baf11e1423287d4138b07c8f2fca95ef51..e96ac4f5766eb601886e3b6574c27044fb33b81a 100644 (file)
@@ -30,11 +30,6 @@ class Fimfiction extends BasicSupport_Deprecated {
                return true;
        }
 
-       @Override
-       public String getSourceName() {
-               return "FimFiction.net";
-       }
-
        @Override
        protected MetaData getMeta(URL source, InputStream in) throws IOException {
                MetaData meta = new MetaData();
@@ -43,9 +38,9 @@ class Fimfiction extends BasicSupport_Deprecated {
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate(getDate(reset(in)));
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en");
index a99986f27e1f4e50aa88b1b0de27cbd9bd833cc8..a64e4c082f4eccbe5e1ae9fcc88745306207a5fc 100644 (file)
@@ -84,11 +84,6 @@ class FimfictionApi extends BasicSupport {
                return true;
        }
 
-       @Override
-       public String getSourceName() {
-               return "FimFiction.net";
-       }
-
        /**
         * Extract the full JSON data we will later use to build the {@link Story}.
         * 
@@ -137,9 +132,9 @@ class FimfictionApi extends BasicSupport {
                meta.setAuthor(getKeyJson(json, 0, "type", "user", "name"));
                meta.setDate(getKeyJson(json, 0, "type", "story", "date_published"));
                meta.setTags(getTags());
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("en");
@@ -150,10 +145,11 @@ class FimfictionApi extends BasicSupport {
                String coverImageLink = getKeyJson(json, 0, "type", "story",
                                "cover_image", "full");
                if (!coverImageLink.trim().isEmpty()) {
-                       InputStream in = null;
+                       URL coverImageUrl = new URL(coverImageLink.trim());
+
+                       InputStream in = Instance.getCache()
+                                       .open(coverImageUrl, this, true);
                        try {
-                               URL coverImageUrl = new URL(coverImageLink.trim());
-                               in = Instance.getCache().open(coverImageUrl, this, true);
                                meta.setCover(new Image(in));
                        } finally {
                                in.close();
index 5fe28397e66936698a73dec7c64787bc08a43730..c27dd32f181651186446bbe597c09b6f6b7d8c5b 100644 (file)
@@ -14,11 +14,6 @@ import be.nikiroo.fanfix.Instance;
  * @author niki
  */
 class Html extends InfoText {
-       @Override
-       public String getSourceName() {
-               return "html";
-       }
-
        @Override
        protected boolean supports(URL url) {
                try {
index 817345e028fce27bf6f53f397b3eaeb4a55ca545..57f021f203e68450c9926e372fb02f4d484fde9c 100644 (file)
@@ -14,7 +14,7 @@ import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.bundles.Config;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.utils.Image;
-import be.nikiroo.utils.MarkableFileInputStream;
+import be.nikiroo.utils.streams.MarkableFileInputStream;
 
 // not complete: no "description" tag
 public class InfoReader {
@@ -25,8 +25,7 @@ public class InfoReader {
                }
 
                if (infoFile.exists()) {
-                       InputStream in = new MarkableFileInputStream(new FileInputStream(
-                                       infoFile));
+                       InputStream in = new MarkableFileInputStream(infoFile);
                        try {
                                return createMeta(infoFile.toURI().toURL(), in, withCover);
                        } finally {
index 37f447aeb721a970fe14e6933a73dd396b1823d4..42e2c13b6f75a1e32afa10f560361c1670ea9572 100644 (file)
@@ -16,11 +16,6 @@ import be.nikiroo.fanfix.data.MetaData;
  * @author niki
  */
 class InfoText extends Text {
-       @Override
-       public String getSourceName() {
-               return "info-text";
-       }
-
        protected File getInfoFile() {
                return new File(assureNoTxt(getSourceFile()).getPath() + ".info");
        }
index bd5816a63641c57e0fdb6ba7f7aa0f2e86e652bb..dae2d314f900d79a70c5d6237667d007a9a616dd 100644 (file)
@@ -28,11 +28,6 @@ class MangaFox extends BasicSupport {
                return true;
        }
 
-       @Override
-       public String getSourceName() {
-               return "MangaFox.me";
-       }
-
        @Override
        protected MetaData getMeta() throws IOException {
                MetaData meta = new MetaData();
@@ -63,9 +58,9 @@ class MangaFox extends BasicSupport {
                        meta.setDate(StringUtils.unhtml(table.get(0).text()).trim());
                        meta.setTags(explode(table.get(3).text()));
                }
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("en");
@@ -341,8 +336,8 @@ class MangaFox extends BasicSupport {
         */
        private InputStream openEx(String url) throws IOException {
                try {
-                       return Instance.getCache().open(new URL(url), this, true,
-                                       withoutQuery(url));
+                       return Instance.getCache().open(new URL(url), withoutQuery(url),
+                                       this, true);
                } catch (Exception e) {
                        // second chance
                        try {
@@ -350,8 +345,8 @@ class MangaFox extends BasicSupport {
                        } catch (InterruptedException ee) {
                        }
 
-                       return Instance.getCache().open(new URL(url), this, true,
-                                       withoutQuery(url));
+                       return Instance.getCache().open(new URL(url), withoutQuery(url),
+                                       this, true);
                }
        }
 
index 0351622e8ef66b66d6563c6a0920e3f4f8a8edb2..1ba51bc0f5d8a5c32a0341526a5aac142ae3fff6 100644 (file)
@@ -2,11 +2,11 @@ package be.nikiroo.fanfix.supported;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.MalformedURLException;
 import java.net.URL;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.AbstractMap;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map.Entry;
 
@@ -26,24 +26,17 @@ class MangaLel extends BasicSupport {
                return true;
        }
 
-       @Override
-       public String getSourceName() {
-               return "MangaLel.com";
-       }
-
        @Override
        protected MetaData getMeta() throws IOException {
                MetaData meta = new MetaData();
 
-               String[] authorDateTag = getAuthorDateTag();
-
                meta.setTitle(getTitle());
-               meta.setAuthor(authorDateTag[0]);
-               meta.setDate(authorDateTag[1]);
-               meta.setTags(explode(authorDateTag[2]));
-               meta.setSource(getSourceName());
+               meta.setAuthor(getAuthor());
+               meta.setDate(getDate());
+               meta.setTags(getTags());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("fr");
@@ -57,131 +50,153 @@ class MangaLel extends BasicSupport {
 
        private String getTitle() {
                Element doc = getSourceNode();
-               Element h2 = doc.getElementsByClass("widget-title").first();
-               if (h2 != null) {
-                       return StringUtils.unhtml(h2.text()).trim();
+               Element h4 = doc.getElementsByTag("h4").first();
+               if (h4 != null) {
+                       return StringUtils.unhtml(h4.text()).trim();
                }
 
                return null;
        }
 
-       // 0 = author
-       // 1 = date
-       // 2 = tags
-       private String[] getAuthorDateTag() {
-               String[] tab = new String[3];
+       private String getAuthor() {
+               Element doc = getSourceNode();
+               Element tabEls = doc.getElementsByClass("presentation-projet").first();
+               if (tabEls != null) {
+                       String[] tab = tabEls.outerHtml().split("<br>");
+                       return getVal(tab, 1);
+               }
 
+               return "";
+       }
+
+       private List<String> getTags() {
                Element doc = getSourceNode();
-               Element tabEls = doc.getElementsByClass("dl-horizontal").first();
-               int prevOk = 0;
-               for (Element tabEl : tabEls.children()) {
-                       String txt = tabEl.text().trim();
-                       if (prevOk > 0) {
-                               if (tab[prevOk - 1] == null) {
-                                       tab[prevOk - 1] = "";
-                               } else {
-                                       tab[prevOk - 1] += ", ";
-                               }
+               Element tabEls = doc.getElementsByClass("presentation-projet").first();
+               if (tabEls != null) {
+                       String[] tab = tabEls.outerHtml().split("<br>");
+                       List<String> tags = new ArrayList<String>();
+                       for (String tag : getVal(tab, 3).split(" ")) {
+                               tags.add(tag);
+                       }
+                       return tags;
+               }
 
-                               tab[prevOk - 1] += txt;
-                               prevOk = 0;
-                       } else {
-                               if (txt.equals("Auteur(s)") || txt.equals("Artist(s)")) {
-                                       prevOk = 1;
-                               } else if (txt.equals("Date de sortie")) {
-                                       prevOk = 2;
-                               } else if (txt.equals("Type") || txt.equals("Catégories")) {
-                                       prevOk = 3;
-                               } else {
-                                       prevOk = 0;
+               return new ArrayList<String>();
+
+       }
+
+       private String getDate() {
+               Element doc = getSourceNode();
+               Element table = doc.getElementsByClass("table").first();
+
+               // We take the first date we find
+               String value = "";
+               if (table != null) {
+                       Elements els;
+                       els = table.getElementsByTag("tr");
+                       if (els.size() >= 2) {
+                               els = els.get(1).getElementsByTag("td");
+                               if (els.size() >= 3) {
+                                       value = StringUtils.unhtml(els.get(2).text()).trim();
                                }
                        }
                }
 
-               for (int i = 0; i < 3; i++) {
-                       String list = "";
-                       for (String item : explode(tab[i])) {
-                               if (!list.isEmpty()) {
-                                       list = list + ", ";
-                               }
-                               list += item;
+               if (!value.isEmpty()) {
+                       try {
+                               long time = StringUtils.toTime(value);
+                               value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+                                               .format(time);
+                       } catch (ParseException e) {
                        }
-                       tab[i] = list;
                }
 
-               return tab;
+               return value;
        }
 
        @Override
        protected String getDesc() {
-               String desc = null;
-
                Element doc = getSourceNode();
-               Element title = doc.getElementsByClass("well").first();
-               if (title != null) {
-                       desc = StringUtils.unhtml(title.text()).trim();
-                       if (desc.startsWith("Résumé")) {
-                               desc = desc.substring("Résumé".length()).trim();
-                       }
+               Element tabEls = doc.getElementsByClass("presentation-projet").first();
+               if (tabEls != null) {
+                       String[] tab = tabEls.outerHtml().split("<br>");
+                       return getVal(tab, 4);
                }
 
-               return desc;
+               return "";
        }
 
        private Image getCover() {
                Element doc = getSourceNode();
-               Element cover = doc.getElementsByClass("img-responsive").first();
+               Element container = doc.getElementsByClass("container").first();
+
+               if (container != null) {
+
+                       Elements imgs = container.getElementsByTag("img");
+                       Element img = null;
+                       if (imgs.size() >= 1) {
+                               img = imgs.get(0);
+                               if (img.hasClass("banniere-team-projet")) {
+                                       img = null;
+                                       if (imgs.size() >= 2) {
+                                               img = imgs.get(1);
+                                       }
+                               }
+                       }
 
-               if (cover != null) {
-                       String coverUrl = cover.absUrl("src");
+                       if (img != null) {
+                               String coverUrl = img.absUrl("src");
 
-                       InputStream coverIn;
-                       try {
-                               coverIn = Instance.getCache().open(new URL(coverUrl), this,
-                                               true);
+                               InputStream coverIn;
                                try {
-                                       return new Image(coverIn);
-                               } finally {
-                                       coverIn.close();
+                                       coverIn = Instance.getCache().open(new URL(coverUrl), this,
+                                                       true);
+                                       try {
+                                               return new Image(coverIn);
+                                       } finally {
+                                               coverIn.close();
+                                       }
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
                                }
-                       } catch (IOException e) {
-                               Instance.getTraceHandler().error(e);
                        }
                }
 
                return null;
        }
 
-       @Override
-       protected List<Entry<String, URL>> getChapters(Progress pg) {
-               List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>();
+       private String getVal(String[] tab, int i) {
+               String val = "";
 
-               int i = 0;
-               Element doc = getSourceNode();
-               Elements chapEls = doc.getElementsByClass("chapters").first()
-                               .getElementsByTag("li");
-               for (Element chapEl : chapEls) {
-                       Element titleEl = chapEl.getElementsByTag("h5").first();
-                       String title = StringUtils.unhtml(titleEl.text()).trim();
+               if (i < tab.length) {
+                       val = StringUtils.unhtml(tab[i]);
+                       int pos = val.indexOf(":");
+                       if (pos >= 0) {
+                               val = val.substring(pos + 1).trim();
+                       }
+               }
 
-                       // because Atril does not support strange file names
-                       title = Integer.toString(chapEls.size() - i);
+               return val;
+       }
 
-                       Element linkEl = chapEl.getElementsByTag("h5").first()
-                                       .getElementsByTag("a").first();
-                       String link = linkEl.absUrl("href");
+       @Override
+       protected List<Entry<String, URL>> getChapters(Progress pg)
+                       throws IOException {
+               List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>();
 
-                       try {
-                               urls.add(new AbstractMap.SimpleEntry<String, URL>(title,
-                                               new URL(link)));
-                       } catch (MalformedURLException e) {
-                               Instance.getTraceHandler().error(e);
+               Element doc = getSourceNode();
+               Element table = doc.getElementsByClass("table").first();
+               if (table != null) {
+                       for (Element tr : table.getElementsByTag("tr")) {
+                               Element a = tr.getElementsByTag("a").first();
+                               if (a != null) {
+                                       String name = StringUtils.unhtml(a.text()).trim();
+                                       URL url = new URL(a.absUrl("href"));
+                                       urls.add(new AbstractMap.SimpleEntry<String, URL>(name, url));
+                               }
                        }
-
-                       i++;
                }
 
-               Collections.reverse(urls);
                return urls;
        }
 
@@ -197,13 +212,16 @@ class MangaLel extends BasicSupport {
                InputStream in = Instance.getCache().open(chapUrl, this, false);
                try {
                        Element pageDoc = DataUtil.load(in, "UTF-8", chapUrl.toString());
-                       Elements linkEls = pageDoc.getElementsByClass("img-responsive");
+                       Element content = pageDoc.getElementById("content");
+                       Elements linkEls = content.getElementsByTag("img");
                        for (Element linkEl : linkEls) {
-                               if (linkEl.hasAttr("data-src")) {
-                                       builder.append("[");
-                                       builder.append(linkEl.absUrl("data-src").trim());
-                                       builder.append("]<br/>");
+                               if (linkEl.absUrl("src").isEmpty()) {
+                                       continue;
                                }
+
+                               builder.append("[");
+                               builder.append(linkEl.absUrl("src"));
+                               builder.append("]<br/>");
                        }
 
                } finally {
@@ -213,32 +231,11 @@ class MangaLel extends BasicSupport {
                return builder.toString();
        }
 
-       /**
-        * Explode an HTML comma-separated list of values into a non-duplicate text
-        * {@link List} .
-        * 
-        * @param values
-        *            the comma-separated values in HTML format
-        * 
-        * @return the full list with no duplicate in text format
-        */
-       private List<String> explode(String values) {
-               List<String> list = new ArrayList<String>();
-               if (values != null && !values.isEmpty()) {
-                       for (String auth : values.split(",")) {
-                               String a = StringUtils.unhtml(auth).trim();
-                               if (!a.isEmpty() && !list.contains(a.trim())) {
-                                       list.add(a);
-                               }
-                       }
-               }
-
-               return list;
-       }
-
        @Override
        protected boolean supports(URL url) {
-               return "manga-lel.com".equals(url.getHost())
-                               || "www.manga-lel.com".equals(url.getHost());
+               // URL structure (the projectId is the manga key):
+               // http://mangas-lecture-en-ligne.fr/index_lel.php?page=presentationProjet&idProjet=999
+
+               return "mangas-lecture-en-ligne.fr".equals(url.getHost());
        }
 }
index 7ded3926625ccf1382989a627b39734cfa8fda9e..ba18949e504f76f72c6a444608dad05f98063523 100644 (file)
@@ -35,8 +35,44 @@ public enum SupportType {
        HTML;
 
        /**
-        * A description of this support type (more information than the
-        * {@link BasicSupport#getSourceName()}).
+        * The name of this support type (a short version).
+        * 
+        * @return the name
+        */
+       public String getSourceName() {
+               switch (this) {
+               case CBZ:
+                       return "cbz";
+               case E621:
+                       return "e621.net";
+               case E_HENTAI:
+                       return "e-hentai.org";
+               case EPUB:
+                       return "epub";
+               case FANFICTION:
+                       return "Fanfiction.net";
+               case FIMFICTION:
+                       return "FimFiction.net";
+               case HTML:
+                       return "html";
+               case INFO_TEXT:
+                       return "info-text";
+               case MANGA_LEL:
+                       return "MangaLEL";
+               case MANGAFOX:
+                       return "MangaFox.me";
+               case TEXT:
+                       return "text";
+               case YIFFSTAR:
+                       return "YiffStar";
+               }
+
+               return "";
+       }
+
+       /**
+        * A description of this support type (more information than
+        * {@link SupportType#getSourceName()}).
         * 
         * @return the description
         */
@@ -51,20 +87,6 @@ public enum SupportType {
                return desc;
        }
 
-       /**
-        * The name of this support type (a short version).
-        * 
-        * @return the name
-        */
-       public String getSourceName() {
-               BasicSupport support = BasicSupport.getSupport(this, null);
-               if (support != null) {
-                       return support.getSourceName();
-               }
-
-               return null;
-       }
-
        @Override
        public String toString() {
                return super.toString().toLowerCase();
index cba23bf8c2f1815d2eddba3ff611ed1951b9e4b6..5a4188a2fe6bfa431ccca2cc6eb57904740d42be 100644 (file)
@@ -19,7 +19,7 @@ import be.nikiroo.fanfix.bundles.Config;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.utils.Image;
 import be.nikiroo.utils.ImageUtils;
-import be.nikiroo.utils.MarkableFileInputStream;
+import be.nikiroo.utils.streams.MarkableFileInputStream;
 import be.nikiroo.utils.Progress;
 
 /**
@@ -67,16 +67,11 @@ class Text extends BasicSupport {
                return false;
        }
 
-       @Override
-       public String getSourceName() {
-               return "text";
-       }
-
        @Override
        protected Document loadDocument(URL source) throws IOException {
                try {
                        sourceFile = new File(source.toURI());
-                       in = new MarkableFileInputStream(new FileInputStream(sourceFile));
+                       in = new MarkableFileInputStream(sourceFile);
                } catch (URISyntaxException e) {
                        throw new IOException("Cannot load the text document: " + source);
                }
@@ -92,7 +87,7 @@ class Text extends BasicSupport {
                meta.setAuthor(getAuthor());
                meta.setDate(getDate());
                meta.setTags(new ArrayList<String>());
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(getSourceFile().toURI().toURL().toString());
                meta.setPublisher("");
                meta.setUuid(getSourceFile().toString());
index 92d44fe9beaa9f997c6d1375af2f4c332b0ee496..a17253a5762b4199a0f2997361da63f92dd009f1 100644 (file)
@@ -26,12 +26,6 @@ import be.nikiroo.utils.StringUtils;
  * @author niki
  */
 class YiffStar extends BasicSupport_Deprecated {
-
-       @Override
-       public String getSourceName() {
-               return "YiffStar";
-       }
-
        @Override
        protected MetaData getMeta(URL source, InputStream in) throws IOException {
                MetaData meta = new MetaData();
@@ -40,9 +34,9 @@ class YiffStar extends BasicSupport_Deprecated {
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate("");
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getSourceName());
+               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getSourceName());
+               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en");
index a3f5221df5c62db03b1e1e970074e036dbdec59a..b731c441860bb6b8d3baf10da8ca8fdaa783fc11 100644 (file)
@@ -394,11 +394,6 @@ class BasicSupportTest extends TestLauncher {
        }
 
        private class BasicSupportEmpty extends BasicSupport_Deprecated {
-               @Override
-               protected String getSourceName() {
-                       return null;
-               }
-
                @Override
                protected boolean supports(URL url) {
                        return false;