# - 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):
#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/
@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
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 \
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."
@[ ! -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"
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; \
- ```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*]
- 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
- ```--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
- ```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*]
- 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
- ```--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
- [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
- [ ] 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
+++ /dev/null
-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'
-}
-
-
+++ /dev/null
-# 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
+++ /dev/null
-// 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
-}
+++ /dev/null
-# 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
+++ /dev/null
-#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
+++ /dev/null
-#!/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 "$@"
+++ /dev/null
-@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
+++ /dev/null
-include ':app'
# 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
# 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
# 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
# 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
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
ENABLED=true
USER=fanfix
-
JAR=/path/to/fanfix.jar
-PINCODE="my password"
-PORT=12000
FPID=/tmp/fanfix.pid
OUT=/var/log/fanfix
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
;;
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
+++ /dev/null
-<?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
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;
*/
public class DataLoader {
private Downloader downloader;
- private Cache downloadCache;
+ private Downloader downloaderNoCache;
private Cache cache;
/**
*/
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();
}
/**
*/
public DataLoader(String UA) {
downloader = new Downloader(UA);
- downloadCache = null;
+ downloaderNoCache = downloader;
cache = new CacheMemory();
}
*/
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);
}
}
/**
* 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
*/
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);
}
/**
*
* @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);
}
/**
}
}
- return downloader.open(url, currentReferer, cookiesValues, postParams,
- getParams, oauth);
+ return downloaderNoCache.open(url, currentReferer, cookiesValues,
+ postParams, getParams, oauth);
}
/**
*/
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();
}
}
*
*/
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);
}
/**
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;
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();
// 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);
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")
trans.deleteFile(configDir);
}
- if (checkEnv("NOUTF")) {
+ Boolean noutf = checkEnv("NOUTF");
+ if (noutf != null && noutf) {
trans.setUnicode(false);
transGui.setUnicode(false);
}
+ getFile(libDir), e));
}
} else {
+ Exception ex = null;
int pos = remoteLib.lastIndexOf(":");
if (pos >= 0) {
String port = remoteLib.substring(pos + 1).trim();
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));
}
}
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");
*
* @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();
|| "y".equals(value)) {
return true;
}
+
+ return false;
}
- return false;
+ return null;
}
}
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;
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;
*/
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
}
/**
* </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>
* <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>
*
* 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;
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;
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;
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) {
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);
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);
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 {
}
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)
* @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;
}
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) {
}
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")
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, //
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")
@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")
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
# 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 =
\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\
\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\
# 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
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
# 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
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
package be.nikiroo.fanfix.data;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
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>();
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;
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(
package be.nikiroo.fanfix.data;
+import java.io.Serializable;
+
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.
*
package be.nikiroo.fanfix.data;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
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>();
/**
* This package contains the data structure used by the program, without the
* logic behind them.
+ * <p>
+ * All the classes inside are serializable.
*
* @author niki
*/
* @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);
+ }
}
/**
* @return the current status
*/
public Status getStatus() {
- return Status.READY;
+ return Status.READ_WRITE;
}
/**
* 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.
* 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.
* 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;
* 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;
* 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;
}
* 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;
}
* 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.
* 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
* 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
*
* @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.
* 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();
* </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;
* 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();
* <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>>();
* @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>();
* 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);
}
* 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();
* 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();
* 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())) {
* 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) {
* 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();
}
@Override
- protected List<MetaData> getMetas(Progress pg) {
+ protected List<MetaData> getMetas(Progress pg) throws IOException {
if (pg == null) {
pg = new Progress();
}
}
@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);
}
@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();
}
}
@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();
}
}
@Override
- public Image getCover(final String luid) {
+ public Image getCover(final String luid) throws IOException {
if (isCached(luid)) {
return cacheLib.getCover(luid);
}
}
@Override
- public Image getSourceCover(String source) {
+ public Image getSourceCover(String source) throws IOException {
Image custom = getCustomSourceCover(source);
if (custom != null) {
return custom;
}
@Override
- public Image getAuthorCover(String author) {
+ public Image getAuthorCover(String author) throws IOException {
Image custom = getCustomAuthorCover(author);
if (custom != null) {
return custom;
}
@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);
}
@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);
}
@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())) {
* @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;
+ }
}
/**
}
@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);
}
@Override
- public Image getCover(String luid) {
+ public Image getCover(String luid) throws IOException {
MetaData meta = getInfo(luid);
if (meta != null) {
if (meta.getCover() != null) {
}
@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));
}
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;
* @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
* 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) {
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];
}
@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();
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]);
@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
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");
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
/**
* 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);
}
@Override
- protected List<MetaData> getMetas(Progress pg) {
+ protected List<MetaData> getMetas(Progress pg) throws IOException {
return getMetasList("*", pg);
}
* @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];
+ }
}
}
--- /dev/null
+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;
+ }
+}
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;
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
* 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];
}
}
- 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) {
}
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);
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);
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]);
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]);
(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);
}
@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);
+ }
}
/**
*
* @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
action.send(new Integer[] { min, max, relativeProgress });
action.rec();
} catch (Exception e) {
- Instance.getTraceHandler().error(e);
+ getTraceHandler().error(e);
}
lastTime[0] = new Date().getTime();
}
// 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);
}
}
}
* 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
*/
}
@Override
- public synchronized Story getStory(Progress pg) {
+ public synchronized Story getStory(Progress pg) throws IOException {
if (story == null) {
story = getLibrary().getStory(meta.getLuid(), pg);
}
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());
}
}
- 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 = "";
}
// :(
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;
/**
* 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
* @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
+++ /dev/null
-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);
- }
- }
-}
+++ /dev/null
-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
+++ /dev/null
-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);
- }
- }
-}
+++ /dev/null
-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();
- }
- });
- }
-}
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.
}
@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 = "";
+ 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++;
+ }
+ }
}
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'
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.");
+ }
+ }
}
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;
/**
}
@Override
- public Story getStory(Progress pg) {
+ public Story getStory(Progress pg) throws IOException {
return reader.getStory(pg);
}
@Override
public void browse(String source) {
- reader.browse(source);
+ try {
+ reader.browse(source);
+ } catch (IOException e) {
+ Instance.getTraceHandler().error(e);
+ }
}
@Override
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}.
} 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;
*/
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);
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;
}
}
+ @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.
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
* 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:
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
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);
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);
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;
- }
}
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}.
*
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 {
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;
*/
public GuiReaderFrame(GuiReader reader, String type) {
super(getAppTitle(reader.getLibrary().getLibraryName()));
-
+
this.reader = reader;
mainPanel = new GuiReaderMainPanel(this, type);
@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;
}
@Override
- public void createMenu(boolean libOk) {
+ public void createMenu(Status status) {
invalidate();
JMenuBar bar = new JMenuBar();
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();
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(
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);
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);
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();
}
};
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);
/**
* 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(
/**
* 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);
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);
}
}
/**
* 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);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
- createMenu(true);
+ createMenu(reader.getLibrary().getStatus());
}
});
}
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));
}
}
});
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();
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);
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();
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);
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;
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;
* 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));
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() {
});
}
+ /**
+ * 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;
+ }
}
/**
*/
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);
}
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());
}
pane.repaint();
validate();
repaint();
+
+ computeItemsPerLine();
}
/**
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()) {
* @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()) {
books.get(previousIndex).setSelected(false);
}
- if (index >= 0) {
+ if (index >= 0 && !books.isEmpty()) {
books.get(index).setSelected(true);
}
}
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);
+ }
+ }
}
* <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
pane = new JPanel();
pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
+ JScrollPane scroll = new JScrollPane(pane);
Integer icolor = Instance.getUiConfig().getColor(
UiConfig.BACKGROUND_COLOR);
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);
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(
* @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) {
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));
}
--- /dev/null
+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 > 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 < 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 > 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 <HTML> and
+ * </HTML>.
+ * <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 = " <B>Page <SPAN COLOR='#444466'>%d</SPAN> ";
+ if (max >= 0) {
+ base += "/ %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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ });
+ }
+}
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;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
-import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
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.
* 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);
}
/**
* 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);
}
}
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;
* 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);
main.invalidate();
}
- setChapter(story.getMeta().getResume());
+ setChapter(meta.getResume());
}
/**
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
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}.
*
* @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();
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.
* 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
*/
// try for files
if (source != null) {
try {
-
String relPath = null;
String absPath = null;
try {
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;
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,
return url.toString().toLowerCase().endsWith(".cbz");
}
- @Override
- public String getSourceName() {
- return "cbz";
- }
-
@Override
protected String getDataPrefix() {
return "";
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()) {
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());
+ }
}
}
* @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();
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");
* @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();
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)));
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;
/**
private URL fakeSource;
private InputStream fakeIn;
- @Override
- public String getSourceName() {
- return "epub";
- }
-
public File getSourceFileOriginal() {
return super.getSourceFile();
}
}
if (tmp.exists()) {
- this.fakeIn = new MarkableFileInputStream(new FileInputStream(
- tmp));
+ this.fakeIn = new MarkableFileInputStream(tmp);
}
if (tmpInfo.exists()) {
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);
return true;
}
- @Override
- public String getSourceName() {
- return "Fanfiction.net";
- }
-
@Override
protected MetaData getMeta(URL source, InputStream in) throws IOException {
MetaData meta = new MetaData();
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!
}
}
- return null;
+ return "";
}
private String getAuthor(InputStream in) {
return true;
}
- @Override
- public String getSourceName() {
- return "FimFiction.net";
- }
-
@Override
protected MetaData getMeta(URL source, InputStream in) throws IOException {
MetaData meta = new MetaData();
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");
return true;
}
- @Override
- public String getSourceName() {
- return "FimFiction.net";
- }
-
/**
* Extract the full JSON data we will later use to build the {@link Story}.
*
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");
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();
* @author niki
*/
class Html extends InfoText {
- @Override
- public String getSourceName() {
- return "html";
- }
-
@Override
protected boolean supports(URL url) {
try {
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 {
}
if (infoFile.exists()) {
- InputStream in = new MarkableFileInputStream(new FileInputStream(
- infoFile));
+ InputStream in = new MarkableFileInputStream(infoFile);
try {
return createMeta(infoFile.toURI().toURL(), in, withCover);
} finally {
* @author niki
*/
class InfoText extends Text {
- @Override
- public String getSourceName() {
- return "info-text";
- }
-
protected File getInfoFile() {
return new File(assureNoTxt(getSourceFile()).getPath() + ".info");
}
return true;
}
- @Override
- public String getSourceName() {
- return "MangaFox.me";
- }
-
@Override
protected MetaData getMeta() throws IOException {
MetaData meta = new MetaData();
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");
*/
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 {
} 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);
}
}
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;
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");
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;
}
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 {
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());
}
}
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
*/
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();
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;
/**
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);
}
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());
* @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();
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");
}
private class BasicSupportEmpty extends BasicSupport_Deprecated {
- @Override
- protected String getSourceName() {
- return null;
- }
-
@Override
protected boolean supports(URL url) {
return false;