Merge branch 'dev' into fix/hide-subscribe-btn

This commit is contained in:
Alex Ling 2022-07-18 19:42:23 +08:00 committed by GitHub
commit cb3df432d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 216 additions and 49 deletions

View File

@ -12,3 +12,4 @@ Layout/LineLength:
MaxLength: 80 MaxLength: 80
Excluded: Excluded:
- src/routes/api.cr - src/routes/api.cr
- spec/plugin_spec.cr

View File

@ -14,6 +14,7 @@ const readerComponent = () => {
margin: 30, margin: 30,
preloadLookahead: 3, preloadLookahead: 3,
enableRightToLeft: false, enableRightToLeft: false,
fitType: 'vert',
/** /**
* Initialize the component by fetching the page dimensions * Initialize the component by fetching the page dimensions
@ -29,14 +30,16 @@ const readerComponent = () => {
return { return {
id: i + 1, id: i + 1,
url: `${base_url}api/page/${tid}/${eid}/${i+1}`, url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
width: d.width, width: d.width == 0 ? "100%" : d.width,
height: d.height, height: d.height == 0 ? "100%" : d.height,
}; };
}); });
const avgRatio = this.items.reduce((acc, cur) => { // Note: for image types not supported by image_size.cr, the width and height will be 0, and so `avgRatio` will be `Infinity`.
// TODO: support more image types in image_size.cr
const avgRatio = dimensions.reduce((acc, cur) => {
return acc + cur.height / cur.width return acc + cur.height / cur.width
}, 0) / this.items.length; }, 0) / dimensions.length;
console.log(avgRatio); console.log(avgRatio);
this.longPages = avgRatio > 2; this.longPages = avgRatio > 2;
@ -63,6 +66,11 @@ const readerComponent = () => {
this.preloadImage(this.items[idx - 1].url); this.preloadImage(this.items[idx - 1].url);
} }
const savedFitType = localStorage.getItem('fitType');
if (savedFitType) {
this.fitType = savedFitType;
$('#fit-select').val(savedFitType);
}
const savedFlipAnimation = localStorage.getItem('enableFlipAnimation'); const savedFlipAnimation = localStorage.getItem('enableFlipAnimation');
this.enableFlipAnimation = savedFlipAnimation === null || savedFlipAnimation === 'true'; this.enableFlipAnimation = savedFlipAnimation === null || savedFlipAnimation === 'true';
@ -333,6 +341,11 @@ const readerComponent = () => {
this.toPage(this.selectedIndex); this.toPage(this.selectedIndex);
}, },
fitChanged(){
this.fitType = $('#fit-select').val();
localStorage.setItem('fitType', this.fitType);
},
preloadLookaheadChanged() { preloadLookaheadChanged() {
localStorage.setItem('preloadLookahead', this.preloadLookahead); localStorage.setItem('preloadLookahead', this.preloadLookahead);
}, },

View File

View File

@ -0,0 +1,6 @@
{
"id": "test",
"title": "Test Plugin",
"placeholder": "placeholder",
"wait_seconds": 1
}

View File

@ -1,14 +1,31 @@
require "./spec_helper" require "./spec_helper"
describe Config do describe Config do
it "creates config if it does not exist" do it "creates default config if it does not exist" do
with_default_config do |_, path| with_default_config do |config, path|
File.exists?(path).should be_true File.exists?(path).should be_true
config.port.should eq 9000
end end
end end
it "correctly loads config" do it "correctly loads config" do
config = Config.load "spec/asset/test-config.yml" config = Config.load "spec/asset/test-config.yml"
config.port.should eq 3000 config.port.should eq 3000
config.base_url.should eq "/"
end
it "correctly reads config defaults from ENV" do
ENV["LOG_LEVEL"] = "debug"
config = Config.load "spec/asset/test-config.yml"
config.log_level.should eq "debug"
config.base_url.should eq "/"
end
it "correctly handles ENV truthiness" do
ENV["CACHE_ENABLED"] = "false"
config = Config.load "spec/asset/test-config.yml"
config.cache_enabled.should be_false
config.cache_log_enabled.should be_true
config.disable_login.should be_false
end end
end end

70
spec/plugin_spec.cr Normal file
View File

@ -0,0 +1,70 @@
require "./spec_helper"
describe Plugin do
describe "helper functions" do
it "mango.text" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.text('<a href="https://github.com">Click Me<a>');
JS
res.should eq "Click Me"
end
end
it "mango.text returns empty string when no text" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.text('<img src="https://github.com" />');
JS
res.should eq ""
end
end
it "mango.css" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.css('<ul><li class="test">A</li><li class="test">B</li><li>C</li></ul>', 'li.test');
JS
res.should eq ["<li class=\"test\">A</li>", "<li class=\"test\">B</li>"]
end
end
it "mango.css returns empty array when no match" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.css('<ul><li class="test">A</li><li class="test">B</li><li>C</li></ul>', 'li.noclass');
JS
res.should eq [] of String
end
end
it "mango.attribute" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.attribute('<a href="https://github.com">Click Me<a>', 'href');
JS
res.should eq "https://github.com"
end
end
it "mango.attribute returns undefined when no match" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.attribute('<div />', 'href') === undefined;
JS
res.should be_true
end
end
# https://github.com/hkalexling/Mango/issues/320
it "mango.attribute handles tags in attribute values" do
with_plugin do |plugin|
res = plugin.eval <<-JS
mango.attribute('<div data-a="<img />" data-b="test" />', 'data-b');
JS
res.should eq "test"
end
end
end
end

View File

@ -3,6 +3,7 @@ require "../src/queue"
require "../src/server" require "../src/server"
require "../src/config" require "../src/config"
require "../src/main_fiber" require "../src/main_fiber"
require "../src/plugin/plugin"
class State class State
@@hash = {} of String => String @@hash = {} of String => String
@ -54,3 +55,10 @@ def with_storage
end end
end end
end end
def with_plugin
with_default_config do
plugin = Plugin.new "test", "spec/asset/plugins"
yield plugin
end
end

View File

@ -1,31 +1,51 @@
require "yaml" require "yaml"
class Config class Config
private OPTIONS = {
"host" => "0.0.0.0",
"port" => 9000,
"base_url" => "/",
"session_secret" => "mango-session-secret",
"library_path" => "~/mango/library",
"library_cache_path" => "~/mango/library.yml.gz",
"db_path" => "~/mango.db",
"queue_db_path" => "~/mango/queue.db",
"scan_interval_minutes" => 5,
"thumbnail_generation_interval_hours" => 24,
"log_level" => "info",
"upload_path" => "~/mango/uploads",
"plugin_path" => "~/mango/plugins",
"download_timeout_seconds" => 30,
"cache_enabled" => true,
"cache_size_mbs" => 50,
"cache_log_enabled" => true,
"disable_login" => false,
"default_username" => "",
"auth_proxy_header_name" => "",
"plugin_update_interval_hours" => 24,
}
include YAML::Serializable include YAML::Serializable
@[YAML::Field(ignore: true)] @[YAML::Field(ignore: true)]
property path = "" property path : String = ""
property host = "0.0.0.0"
property port : Int32 = 9000 # Go through the options constant above and define them as properties.
property base_url = "/" # Allow setting the default values through environment variables.
property session_secret = "mango-session-secret" # Overall precedence: config file > environment variable > default value
property library_path = "~/mango/library" {% begin %}
property library_cache_path = "~/mango/library.yml.gz" {% for k, v in OPTIONS %}
property db_path = "~/mango/mango.db" {% if v.is_a? StringLiteral %}
property queue_db_path = "~/mango/queue.db" property {{k.id}} : String = ENV[{{k.upcase}}]? || {{ v }}
property scan_interval_minutes : Int32 = 5 {% elsif v.is_a? NumberLiteral %}
property thumbnail_generation_interval_hours : Int32 = 24 property {{k.id}} : Int32 = (ENV[{{k.upcase}}]? || {{ v.id }}).to_i
property log_level = "info" {% elsif v.is_a? BoolLiteral %}
property upload_path = "~/mango/uploads" property {{k.id}} : Bool = env_is_true? {{ k.upcase }}, {{ v.id }}
property plugin_path = "~/mango/plugins" {% else %}
property download_timeout_seconds : Int32 = 30 raise "Unknown type in config option: {{ v.class_name.id }}"
property cache_enabled = true {% end %}
property cache_size_mbs = 50 {% end %}
property cache_log_enabled = true {% end %}
property disable_login = false
property default_username = ""
property auth_proxy_header_name = ""
property plugin_update_interval_hours : Int32 = 24
@@singlet : Config? @@singlet : Config?
@ -38,7 +58,7 @@ class Config
end end
def self.load(path : String?) def self.load(path : String?)
path = "~/.config/mango/config.yml" if path.nil? path = (ENV["CONFIG_PATH"]? || "~/.config/mango/config.yml") if path.nil?
cfg_path = File.expand_path path, home: true cfg_path = File.expand_path path, home: true
if File.exists? cfg_path if File.exists? cfg_path
config = self.from_yaml File.read cfg_path config = self.from_yaml File.read cfg_path

View File

@ -632,6 +632,16 @@ class Title
if last_read_entry && last_read_entry.finished? username if last_read_entry && last_read_entry.finished? username
last_read_entry = last_read_entry.next_entry username last_read_entry = last_read_entry.next_entry username
if last_read_entry.nil?
# The last entry is finished. Return the first unfinished entry
# (if any)
sorted_entries(username).each do |e|
unless e.finished? username
last_read_entry = e
break
end
end
end
end end
last_read_entry last_read_entry

View File

@ -105,9 +105,10 @@ class Plugin
getter js_path = "" getter js_path = ""
getter storage_path = "" getter storage_path = ""
def self.build_info_ary def self.build_info_ary(dir : String? = nil)
@@info_ary.clear @@info_ary.clear
dir = Config.current.plugin_path dir ||= Config.current.plugin_path
Dir.mkdir_p dir unless Dir.exists? dir Dir.mkdir_p dir unless Dir.exists? dir
Dir.each_child dir do |f| Dir.each_child dir do |f|
@ -160,8 +161,8 @@ class Plugin
list.save list.save
end end
def initialize(id : String) def initialize(id : String, dir : String? = nil)
Plugin.build_info_ary Plugin.build_info_ary dir
@info = @@info_ary.find &.id.== id @info = @@info_ary.find &.id.== id
if @info.nil? if @info.nil?
@ -319,7 +320,7 @@ class Plugin
json json
end end
private def eval(str) def eval(str)
@rt.eval str @rt.eval str
rescue e : Duktape::SyntaxError rescue e : Duktape::SyntaxError
raise SyntaxError.new e.message raise SyntaxError.new e.message
@ -448,9 +449,15 @@ class Plugin
env = Duktape::Sandbox.new ptr env = Duktape::Sandbox.new ptr
html = env.require_string 0 html = env.require_string 0
str = XML.parse(html).inner_text begin
parser = Myhtml::Parser.new html
str = parser.body!.children.first.inner_text
env.push_string str
rescue
env.push_string ""
end
env.push_string str
env.call_success env.call_success
end end
sbx.put_prop_string -2, "text" sbx.put_prop_string -2, "text"
@ -461,8 +468,9 @@ class Plugin
name = env.require_string 1 name = env.require_string 1
begin begin
attr = XML.parse(html).first_element_child.not_nil![name] parser = Myhtml::Parser.new html
env.push_string attr attr = parser.body!.children.first.attribute_by name
env.push_string attr.not_nil!
rescue rescue
env.push_undefined env.push_undefined
end end

View File

@ -64,11 +64,12 @@ class Dir
path = File.join dirname, fn path = File.join dirname, fn
if File.directory? path if File.directory? path
signatures << Dir.contents_signature path, cache signatures << Dir.contents_signature path, cache
signatures << fn if DirEntry.is_valid? path
else else
# Only add its signature value to `signatures` when it is a # Only add its signature value to `signatures` when it is a
# supported file # supported file
signatures << fn if ArchiveEntry.is_valid? fn if ArchiveEntry.is_valid?(fn) || is_supported_image_file(fn)
signatures << fn
end
end end
Fiber.yield Fiber.yield
end end

View File

@ -11,6 +11,7 @@ SUPPORTED_IMG_TYPES = %w(
image/avif image/avif
image/gif image/gif
image/svg+xml image/svg+xml
image/jxl
) )
def random_str def random_str
@ -49,6 +50,7 @@ def register_mime_types
# defiend by Crystal in `MIME.DEFAULT_TYPES` # defiend by Crystal in `MIME.DEFAULT_TYPES`
".apng" => "image/apng", ".apng" => "image/apng",
".avif" => "image/avif", ".avif" => "image/avif",
".jxl" => "image/jxl",
}.each do |k, v| }.each do |k, v|
MIME.register k, v MIME.register k, v
end end
@ -93,9 +95,9 @@ class String
end end
end end
def env_is_true?(key : String) : Bool def env_is_true?(key : String, default : Bool = false) : Bool
val = ENV[key.upcase]? || ENV[key.downcase]? val = ENV[key.upcase]? || ENV[key.downcase]?
return false unless val return default unless val
val.downcase.in? "1", "true" val.downcase.in? "1", "true"
end end

View File

@ -5,7 +5,7 @@
<%= render_component "head" %> <%= render_component "head" %>
<body style="position:relative;" x-data="readerComponent()" x-init="init($nextTick)" @resize.window="resized()"> <body style="position:relative;" x-data="readerComponent()" x-init="init($nextTick)" @resize.window="resized()">
<div class="uk-section uk-section-default uk-section-small reader-bg" :style="mode === 'continuous' ? '' : 'padding:0'"> <div class="uk-section uk-section-default uk-section-small reader-bg" :style="mode === 'continuous' ? '' : 'padding:0; position: relative;'">
<div @keydown.window.debounce="keyHandler($event)"></div> <div @keydown.window.debounce="keyHandler($event)"></div>
@ -19,7 +19,7 @@
</div> </div>
<div <div
:class="{'uk-container': true, 'uk-container-small': mode === 'continuous', 'uk-container-expand': mode !== 'continuous'}"> :class="{'uk-container': true, 'uk-container-small': mode === 'continuous', 'uk-container-expand': mode !== 'continuous'}" style="width: fit-content;">
<div x-show="!loading && mode === 'continuous'" x-cloak> <div x-show="!loading && mode === 'continuous'" x-cloak>
<template x-if="!loading && mode === 'continuous'" x-for="item in items"> <template x-if="!loading && mode === 'continuous'" x-for="item in items">
<img <img
@ -40,18 +40,18 @@
<%- end -%> <%- end -%>
</div> </div>
<div x-cloak x-show="!loading && mode !== 'continuous'" class="uk-flex uk-flex-middle" style="height:100vh"> <div x-cloak x-show="!loading && mode !== 'continuous'" class="uk-flex uk-flex-middle" :style="`height:${fitType === 'vert' ? '100vh' : ''}; min-width: fit-content;`">
<img uk-img :class="{ <img uk-img :class="{
'uk-align-center': true, 'uk-align-center': true,
'uk-animation-slide-left': flipAnimation === 'left', 'uk-animation-slide-left': flipAnimation === 'left',
'uk-animation-slide-right': flipAnimation === 'right' 'uk-animation-slide-right': flipAnimation === 'right'
}" :data-src="curItem.url" :width="curItem.width" :height="curItem.height" :id="curItem.id" @click="clickImage($event)" :style="` }" :data-src="curItem.url" :width="curItem.width" :height="curItem.height" :id="curItem.id" @click="clickImage($event)" :style="`
width:${mode === 'width' ? '100vw' : 'auto'}; width:${fitType === 'horz' ? '100vw' : 'auto'};
height:${mode === 'height' ? '100vh' : 'auto'}; height:${fitType === 'vert' ? '100vh' : 'auto'};
margin-bottom:0; margin-bottom:0;
max-width:100%; max-width:${fitType === 'horz' ? '100%' : fitType === 'vert' ? '' : 'none' };
max-height:100%; max-height:${fitType === 'vert' ? '100%' : fitType === 'horz' ? '' : 'none'};
object-fit: contain; object-fit: contain;
`" /> `" />
@ -94,6 +94,17 @@
</div> </div>
</div> </div>
<div class="uk-margin" x-show="mode !== 'continuous'">
<label class="uk-form-label" for="mode-select">Page fit</label>
<div class="uk-form-controls">
<select id="fit-select" class="uk-select" @change="fitChanged()">
<option value="vert">Fit height</option>
<option value="horz">Fit width</option>
<option value="real">Real size</option>
</select>
</div>
</div>
<div class="uk-margin" x-show="mode === 'continuous'"> <div class="uk-margin" x-show="mode === 'continuous'">
<label class="uk-form-label" for="margin-range" x-text="`Page Margin: ${margin}px`"></label> <label class="uk-form-label" for="margin-range" x-text="`Page Margin: ${margin}px`"></label>
<div class="uk-form-controls"> <div class="uk-form-controls">