diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 37d86c6..00f716f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
container:
- image: crystallang/crystal:0.34.0-alpine
+ image: crystallang/crystal:0.35.1-alpine
steps:
- uses: actions/checkout@v2
diff --git a/.gitignore b/.gitignore
index 3a07fc7..17d6647 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ dist
mango
.env
*.md
+public/css/uikit.css
diff --git a/Dockerfile b/Dockerfile
index 6ce10b2..bcbb011 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM crystallang/crystal:0.34.0-alpine AS builder
+FROM crystallang/crystal:0.35.1-alpine AS builder
WORKDIR /Mango
diff --git a/README.md b/README.md
index 9890adf..0a37706 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI
```
- Mango - Manga Server and Web Reader. Version 0.6.1
+ Mango - Manga Server and Web Reader. Version 0.7.0
Usage:
diff --git a/gulpfile.js b/gulpfile.js
index dc6f33d..6d8502a 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,6 +1,7 @@
const gulp = require('gulp');
const minify = require("gulp-babel-minify");
const minifyCss = require('gulp-minify-css');
+const less = require('gulp-less');
gulp.task('minify-js', () => {
return gulp.src('public/js/*.js')
@@ -10,6 +11,12 @@ gulp.task('minify-js', () => {
.pipe(gulp.dest('dist/js'));
});
+gulp.task('less', () => {
+ return gulp.src('src/assets/*.less')
+ .pipe(less())
+ .pipe(gulp.dest('public/css'));
+});
+
gulp.task('minify-css', () => {
return gulp.src('public/css/*.css')
.pipe(minifyCss())
@@ -21,9 +28,9 @@ gulp.task('img', () => {
.pipe(gulp.dest('dist/img'));
});
-gulp.task('favicon', () => {
- return gulp.src('public/favicon.ico')
+gulp.task('copy-files', () => {
+ return gulp.src('public/*.*')
.pipe(gulp.dest('dist'));
});
-gulp.task('default', gulp.parallel('minify-js', 'minify-css', 'img', 'favicon'));
+gulp.task('default', gulp.parallel('minify-js', gulp.series('less', 'minify-css'), 'img', 'copy-files'));
diff --git a/package.json b/package.json
index dd815a6..98f01b4 100644
--- a/package.json
+++ b/package.json
@@ -8,9 +8,14 @@
"devDependencies": {
"gulp": "^4.0.2",
"gulp-babel-minify": "^0.5.1",
- "gulp-minify-css": "^1.2.4"
+ "gulp-less": "^4.0.1",
+ "gulp-minify-css": "^1.2.4",
+ "less": "^3.11.3"
},
"scripts": {
"uglify": "gulp"
+ },
+ "dependencies": {
+ "uikit": "^3.5.4"
}
}
diff --git a/public/js/download-manager.js b/public/js/download-manager.js
index 4b3f40a..9ab407e 100644
--- a/public/js/download-manager.js
+++ b/public/js/download-manager.js
@@ -24,44 +24,48 @@ const loadConfig = () => {
const remove = (id) => {
var url = base_url + 'api/admin/mangadex/queue/delete';
if (id !== undefined)
- url += '?' + $.param({id: id});
+ url += '?' + $.param({
+ id: id
+ });
console.log(url);
$.ajax({
- type: 'POST',
- url: url,
- dataType: 'json'
- })
- .done(data => {
- if (!data.success && data.error) {
- alert('danger', `Failed to remove job from download queue. Error: ${data.error}`);
- return;
- }
- load();
- })
- .fail((jqXHR, status) => {
- alert('danger', `Failed to remove job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
- });
+ type: 'POST',
+ url: url,
+ dataType: 'json'
+ })
+ .done(data => {
+ if (!data.success && data.error) {
+ alert('danger', `Failed to remove job from download queue. Error: ${data.error}`);
+ return;
+ }
+ load();
+ })
+ .fail((jqXHR, status) => {
+ alert('danger', `Failed to remove job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
+ });
};
const refresh = (id) => {
var url = base_url + 'api/admin/mangadex/queue/retry';
if (id !== undefined)
- url += '?' + $.param({id: id});
+ url += '?' + $.param({
+ id: id
+ });
console.log(url);
$.ajax({
- type: 'POST',
- url: url,
- dataType: 'json'
- })
- .done(data => {
- if (!data.success && data.error) {
- alert('danger', `Failed to restart download job. Error: ${data.error}`);
- return;
- }
- load();
- })
- .fail((jqXHR, status) => {
- alert('danger', `Failed to restart download job. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
- });
+ type: 'POST',
+ url: url,
+ dataType: 'json'
+ })
+ .done(data => {
+ if (!data.success && data.error) {
+ alert('danger', `Failed to restart download job. Error: ${data.error}`);
+ return;
+ }
+ load();
+ })
+ .fail((jqXHR, status) => {
+ alert('danger', `Failed to restart download job. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
+ });
};
const toggle = () => {
$('#pause-resume-btn').attr('disabled', '');
@@ -69,50 +73,52 @@ const toggle = () => {
const action = paused ? 'resume' : 'pause';
const url = `${base_url}api/admin/mangadex/queue/${action}`;
$.ajax({
- type: 'POST',
- url: url,
- dataType: 'json'
- })
- .fail((jqXHR, status) => {
- alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
- })
- .always(() => {
- load();
- $('#pause-resume-btn').removeAttr('disabled');
- });
+ type: 'POST',
+ url: url,
+ dataType: 'json'
+ })
+ .fail((jqXHR, status) => {
+ alert('danger', `Failed to ${action} download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
+ })
+ .always(() => {
+ load();
+ $('#pause-resume-btn').removeAttr('disabled');
+ });
};
const load = () => {
if (loading) return;
loading = true;
console.log('fetching');
$.ajax({
- type: 'GET',
- url: base_url + 'api/admin/mangadex/queue',
- dataType: 'json'
- })
- .done(data => {
- if (!data.success && data.error) {
- alert('danger', `Failed to fetch download queue. Error: ${data.error}`);
- return;
- }
- console.log(data);
- const btnText = data.paused ? "Resume download" : "Pause download";
- $('#pause-resume-btn').text(btnText);
- $('#pause-resume-btn').removeAttr('hidden');
- const rows = data.jobs.map(obj => {
- var cls = 'uk-label ';
- if (obj.status === 'Completed')
- cls += 'uk-label-success';
- if (obj.status === 'Error')
- cls += 'uk-label-danger';
- if (obj.status === 'MissingPages')
- cls += 'uk-label-warning';
+ type: 'GET',
+ url: base_url + 'api/admin/mangadex/queue',
+ dataType: 'json'
+ })
+ .done(data => {
+ if (!data.success && data.error) {
+ alert('danger', `Failed to fetch download queue. Error: ${data.error}`);
+ return;
+ }
+ console.log(data);
+ const btnText = data.paused ? "Resume download" : "Pause download";
+ $('#pause-resume-btn').text(btnText);
+ $('#pause-resume-btn').removeAttr('hidden');
+ const rows = data.jobs.map(obj => {
+ var cls = 'label ';
+ if (obj.status === 'Pending')
+ cls += 'label-pending';
+ if (obj.status === 'Completed')
+ cls += 'label-success';
+ if (obj.status === 'Error')
+ cls += 'label-danger';
+ if (obj.status === 'MissingPages')
+ cls += 'label-warning';
- const info = obj.status_message.length > 0 ? ' ' : '';
- const statusSpan = `${obj.status} ${info} `;
- const dropdown = obj.status_message.length > 0 ? `
${obj.status_message}
` : '';
- const retryBtn = obj.status_message.length > 0 ? ` ` : '';
- return `
+ const info = obj.status_message.length > 0 ? ' ' : '';
+ const statusSpan = `${obj.status} ${info} `;
+ const dropdown = obj.status_message.length > 0 ? `${obj.status_message}
` : '';
+ const retryBtn = obj.status_message.length > 0 ? ` ` : '';
+ return `
${obj.title}
${obj.manga_title}
${obj.success_count}/${obj.pages}
@@ -123,16 +129,16 @@ const load = () => {
${retryBtn}
`;
- });
+ });
- const tbody = `${rows.join('')} `;
- $('tbody').remove();
- $('table').append(tbody);
- })
- .fail((jqXHR, status) => {
- alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
- })
- .always(() => {
- loading = false;
- });
+ const tbody = `${rows.join('')} `;
+ $('tbody').remove();
+ $('table').append(tbody);
+ })
+ .fail((jqXHR, status) => {
+ alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
+ })
+ .always(() => {
+ loading = false;
+ });
};
diff --git a/public/js/theme.js b/public/js/theme.js
index 1ac121b..efdd16c 100644
--- a/public/js/theme.js
+++ b/public/js/theme.js
@@ -22,8 +22,7 @@ const setTheme = themeStr => {
$('.uk-card').addClass('uk-card-secondary');
$('.uk-card').removeClass('uk-card-default');
$('.ui-widget-content').addClass('dark');
- }
- else {
+ } else {
$('html').css('background', '');
$('body').removeClass('uk-light');
$('.uk-card').removeClass('uk-card-secondary');
@@ -39,5 +38,11 @@ const styleModal = () => {
$('.uk-modal-footer').css('background', color);
};
-// do it before document is ready to prevent the initial flash of white
+// do it before document is ready to prevent the initial flash of white on
+// most pages
setTheme(getTheme());
+
+$(() => {
+ // hack for the reader page
+ setTheme(getTheme());
+});
diff --git a/public/js/title.js b/public/js/title.js
index d23cf8f..6211923 100644
--- a/public/js/title.js
+++ b/public/js/title.js
@@ -8,12 +8,12 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
if (percentage === 0) {
$('#continue-btn').attr('hidden', '');
$('#unread-btn').attr('hidden', '');
+ } else if (percentage === 100) {
+ $('#read-btn').attr('hidden', '');
+ $('#continue-btn').attr('hidden', '');
} else {
$('#continue-btn').text('Continue from ' + percentage + '%');
}
- if (percentage === 100) {
- $('#read-btn').attr('hidden', '');
- }
$('#modal-title-link').text(title);
$('#modal-title-link').attr('href', `${base_url}book/${titleID}`);
@@ -35,7 +35,9 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
updateProgress(titleID, entryID, 0);
});
- $('.uk-modal-title.break-word > a').attr('onclick', `edit("${entryID}")`);
+ $('#modal-edit-btn').attr('onclick', `edit("${entryID}")`);
+
+ $('#modal-download-btn').attr('href', `/opds/download/${titleID}/${entryID}`);
UIkit.modal($('#modal')).show();
styleModal();
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..1f53798
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/shard.lock b/shard.lock
index 2d95a6f..dc0322a 100644
--- a/shard.lock
+++ b/shard.lock
@@ -1,46 +1,46 @@
-version: 1.0
+version: 2.0
shards:
ameba:
- github: crystal-ameba/ameba
+ git: https://github.com/crystal-ameba/ameba.git
version: 0.12.1
archive:
- github: hkalexling/archive.cr
+ git: https://github.com/hkalexling/archive.cr.git
version: 0.2.0
baked_file_system:
- github: schovi/baked_file_system
+ git: https://github.com/schovi/baked_file_system.git
version: 0.9.8
clim:
- github: at-grandpa/clim
+ git: https://github.com/at-grandpa/clim.git
version: 0.12.0
db:
- github: crystal-lang/crystal-db
+ git: https://github.com/crystal-lang/crystal-db.git
version: 0.9.0
exception_page:
- github: crystal-loot/exception_page
+ git: https://github.com/crystal-loot/exception_page.git
version: 0.1.4
kemal:
- github: kemalcr/kemal
- version: 0.26.1
+ git: https://github.com/kemalcr/kemal.git
+ version: 0.26.1+git.commit.a8c0f09b858162bd13c96663febef5527b322a32
kemal-session:
- github: kemalcr/kemal-session
+ git: https://github.com/kemalcr/kemal-session.git
version: 0.12.1
kilt:
- github: jeromegn/kilt
+ git: https://github.com/jeromegn/kilt.git
version: 0.4.0
radix:
- github: luislavena/radix
+ git: https://github.com/luislavena/radix.git
version: 0.3.9
sqlite3:
- github: crystal-lang/crystal-sqlite3
+ git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.16.0
diff --git a/shard.yml b/shard.yml
index b9e2975..c8ed636 100644
--- a/shard.yml
+++ b/shard.yml
@@ -1,5 +1,5 @@
name: mango
-version: 0.6.1
+version: 0.7.0
authors:
- Alex Ling
@@ -8,13 +8,14 @@ targets:
mango:
main: src/mango.cr
-crystal: 0.34.0
+crystal: 0.35.0
license: MIT
dependencies:
kemal:
github: kemalcr/kemal
+ commit: a8c0f09b858162bd13c96663febef5527b322a32
kemal-session:
github: kemalcr/kemal-session
sqlite3:
diff --git a/src/archive.cr b/src/archive.cr
index 29dedfb..98423d1 100644
--- a/src/archive.cr
+++ b/src/archive.cr
@@ -1,13 +1,13 @@
-require "zip"
+require "compress/zip"
require "archive"
-# A unified class to handle all supported archive formats. It uses the ::Zip
-# module in crystal standard library if the target file is a zip archive.
-# Otherwise it uses `archive.cr`.
+# A unified class to handle all supported archive formats. It uses the
+# Compress::Zip module in crystal standard library if the target file is a
+# zip archive. Otherwise it uses `archive.cr`.
class ArchiveFile
def initialize(@filename : String)
if [".cbz", ".zip"].includes? File.extname filename
- @archive_file = Zip::File.new filename
+ @archive_file = Compress::Zip::File.new filename
else
@archive_file = Archive::File.new filename
end
@@ -20,16 +20,16 @@ class ArchiveFile
end
def close
- if @archive_file.is_a? Zip::File
- @archive_file.as(Zip::File).close
+ if @archive_file.is_a? Compress::Zip::File
+ @archive_file.as(Compress::Zip::File).close
end
end
# Lists all file entries
def entries
- ary = [] of Zip::File::Entry | Archive::Entry
+ ary = [] of Compress::Zip::File::Entry | Archive::Entry
@archive_file.entries.map do |e|
- if (e.is_a? Zip::File::Entry && e.file?) ||
+ if (e.is_a? Compress::Zip::File::Entry && e.file?) ||
(e.is_a? Archive::Entry && e.info.file?)
ary.push e
end
@@ -37,8 +37,8 @@ class ArchiveFile
ary
end
- def read_entry(e : Zip::File::Entry | Archive::Entry) : Bytes?
- if e.is_a? Zip::File::Entry
+ def read_entry(e : Compress::Zip::File::Entry | Archive::Entry) : Bytes?
+ if e.is_a? Compress::Zip::File::Entry
data = nil
e.open do |io|
slice = Bytes.new e.uncompressed_size
diff --git a/src/assets/uikit.less b/src/assets/uikit.less
new file mode 100644
index 0000000..3482a96
--- /dev/null
+++ b/src/assets/uikit.less
@@ -0,0 +1,33 @@
+@import "node_modules/uikit/src/less/uikit.theme.less";
+
+.label {
+ display: inline-block;
+ padding: @label-padding-vertical @label-padding-horizontal;
+ background: @label-background;
+ line-height: @label-line-height;
+ font-size: @label-font-size;
+ color: @label-color;
+ vertical-align: middle;
+ white-space: nowrap;
+ .hook-label;
+}
+
+.label-success {
+ background-color: @label-success-background;
+ color: @label-success-color;
+}
+
+.label-warning {
+ background-color: @label-warning-background;
+ color: @label-warning-color;
+}
+
+.label-danger {
+ background-color: @label-danger-background;
+ color: @label-danger-color;
+}
+
+.label-pending {
+ background-color: @global-secondary-background;
+ color: @global-inverse-color;
+}
diff --git a/src/library.cr b/src/library.cr
index 9eaf4f7..6fdff2d 100644
--- a/src/library.cr
+++ b/src/library.cr
@@ -4,6 +4,8 @@ require "uri"
require "./util"
require "./archive"
+SUPPORTED_IMG_TYPES = ["image/jpeg", "image/png", "image/webp"]
+
struct Image
property data : Bytes
property mime : String
@@ -17,8 +19,7 @@ end
class Entry
property zip_path : String, book : Title, title : String,
size : String, pages : Int32, id : String, title_id : String,
- encoded_path : String, encoded_title : String, mtime : Time,
- date_added : Time
+ encoded_path : String, encoded_title : String, mtime : Time
def initialize(path, @book, @title_id, storage)
@zip_path = path
@@ -28,13 +29,12 @@ class Entry
@size = (File.size path).humanize_bytes
file = ArchiveFile.new path
@pages = file.entries.count do |e|
- ["image/jpeg", "image/png"].includes? \
+ SUPPORTED_IMG_TYPES.includes? \
MIME.from_filename? e.filename
end
file.close
@id = storage.get_id @zip_path, false
@mtime = File.info(@zip_path).modification_time
- @date_added = load_date_added
end
def to_json(json : JSON::Builder)
@@ -74,7 +74,7 @@ class Entry
ArchiveFile.open @zip_path do |file|
page = file.entries
.select { |e|
- ["image/jpeg", "image/png"].includes? \
+ SUPPORTED_IMG_TYPES.includes? \
MIME.from_filename? e.filename
}
.sort { |a, b|
@@ -90,7 +90,19 @@ class Entry
img
end
- private def load_date_added
+ def next_entry
+ idx = @book.entries.index self
+ return nil if idx.nil? || idx == @book.entries.size - 1
+ @book.entries[idx + 1]
+ end
+
+ def previous_entry
+ idx = @book.entries.index self
+ return nil if idx.nil? || idx == 0
+ @book.entries[idx - 1]
+ end
+
+ def date_added
date_added = nil
TitleInfo.new @book.dir do |info|
info_da = info.date_added[@title]?
@@ -103,6 +115,60 @@ class Entry
end
date_added.not_nil! # is it ok to set not_nil! here?
end
+
+ # For backward backward compatibility with v0.1.0, we save entry titles
+ # instead of IDs in info.json
+ def save_progress(username, page)
+ TitleInfo.new @book.dir do |info|
+ if info.progress[username]?.nil?
+ info.progress[username] = {@title => page}
+ else
+ info.progress[username][@title] = page
+ end
+ # save last_read timestamp
+ if info.last_read[username]?.nil?
+ info.last_read[username] = {@title => Time.utc}
+ else
+ info.last_read[username][@title] = Time.utc
+ end
+ info.save
+ end
+ end
+
+ def load_progress(username)
+ progress = 0
+ TitleInfo.new @book.dir do |info|
+ unless info.progress[username]?.nil? ||
+ info.progress[username][@title]?.nil?
+ progress = info.progress[username][@title]
+ end
+ end
+ [progress, @pages].min
+ end
+
+ def load_percentage(username)
+ page = load_progress username
+ page / @pages
+ end
+
+ def load_last_read(username)
+ last_read = nil
+ TitleInfo.new @book.dir do |info|
+ unless info.last_read[username]?.nil? ||
+ info.last_read[username][@title]?.nil?
+ last_read = info.last_read[username][@title]
+ end
+ end
+ last_read
+ end
+
+ def finished?(username)
+ load_progress(username) == @pages
+ end
+
+ def started?(username)
+ load_progress(username) > 0
+ end
end
class Title
@@ -191,6 +257,17 @@ class Title
@title_ids.map { |tid| @library.get_title! tid }
end
+ # Get all entries, including entries in nested titles
+ def deep_entries
+ return @entries if title_ids.empty?
+ @entries + titles.map { |t| t.deep_entries }.flatten
+ end
+
+ def deep_titles
+ return [] of Title if titles.empty?
+ titles + titles.map { |t| t.deep_titles }.flatten
+ end
+
def parents
ary = [] of Title
tid = @parent_id
@@ -199,7 +276,7 @@ class Title
ary << title
tid = title.parent_id
end
- ary
+ ary.reverse
end
def size
@@ -279,7 +356,7 @@ class Title
# Set the reading progress of all entries and nested libraries to 100%
def read_all(username)
@entries.each do |e|
- save_progress username, e.title, e.pages
+ e.save_progress username, e.pages
end
titles.each do |t|
t.read_all username
@@ -289,81 +366,25 @@ class Title
# Set the reading progress of all entries and nested libraries to 0%
def unread_all(username)
@entries.each do |e|
- save_progress username, e.title, 0
+ e.save_progress username, 0
end
titles.each do |t|
t.unread_all username
end
end
- # For backward backward compatibility with v0.1.0, we save entry titles
- # instead of IDs in info.json
- def save_progress(username, entry, page)
- TitleInfo.new @dir do |info|
- if info.progress[username]?.nil?
- info.progress[username] = {entry => page}
- else
- info.progress[username][entry] = page
- end
- # save last_read timestamp
- if info.last_read[username]?.nil?
- info.last_read[username] = {entry => Time.utc}
- else
- info.last_read[username][entry] = Time.utc
- end
- info.save
- end
+ def deep_read_page_count(username) : Int32
+ entries.map { |e| e.load_progress username }.sum +
+ titles.map { |t| t.deep_read_page_count username }.flatten.sum
end
- def load_progress(username, entry)
- progress = 0
- TitleInfo.new @dir do |info|
- unless info.progress[username]?.nil? ||
- info.progress[username][entry]?.nil?
- progress = info.progress[username][entry]
- end
- end
- progress
- end
-
- def load_percentage(username, entry)
- page = load_progress username, entry
- entry_obj = @entries.find { |e| e.title == entry }
- return 0.0 if entry_obj.nil?
- page / entry_obj.pages
+ def deep_total_page_count : Int32
+ entries.map { |e| e.pages }.sum +
+ titles.map { |t| t.deep_total_page_count }.flatten.sum
end
def load_percentage(username)
- return 0.0 if @entries.empty?
- read_pages = total_pages = 0
- @entries.each do |e|
- read_pages += load_progress username, e.title
- total_pages += e.pages
- end
- read_pages / total_pages
- end
-
- def load_last_read(username, entry)
- last_read = nil
- TitleInfo.new @dir do |info|
- unless info.last_read[username]?.nil? ||
- info.last_read[username][entry]?.nil?
- last_read = info.last_read[username][entry]
- end
- end
- last_read
- end
-
- def next_entry(current_entry_obj)
- idx = @entries.index current_entry_obj
- return nil if idx.nil? || idx == @entries.size - 1
- @entries[idx + 1]
- end
-
- def previous_entry(current_entry_obj)
- idx = @entries.index current_entry_obj
- return nil if idx.nil? || idx == 0
- @entries[idx - 1]
+ deep_read_page_count(username) / deep_total_page_count
end
def get_continue_reading_entry(username)
@@ -380,17 +401,6 @@ class Title
latest_read_entry
end
end
-
- # TODO: More concise title?
- def get_last_read_for_continue_reading(username, entry_obj)
- last_read = load_last_read username, entry_obj.title
- # grab from previous entry if current entry hasn't been started yet
- if last_read.nil?
- previous_entry = previous_entry(entry_obj)
- return load_last_read username, previous_entry.title if previous_entry
- end
- last_read
- end
end
class TitleInfo
@@ -470,6 +480,10 @@ class Library
@title_ids.map { |tid| self.get_title!(tid) }
end
+ def deep_titles
+ titles + titles.map { |t| t.deep_titles }.flatten
+ end
+
def to_json(json : JSON::Builder)
json.object do
json.field "dir", @dir
@@ -509,23 +523,37 @@ class Library
end
def get_continue_reading_entries(username)
- # map: get the continue-reading entry or nil for each Title
- # select: select only entries (and ignore Nil's) from the array
- # produced by map
- continue_reading_entries = titles.map { |t|
- get_continue_reading_entry username, t
- }.select Entry
-
- continue_reading = continue_reading_entries.map { |e|
- {
- entry: e,
- percentage: e.book.load_percentage(username, e.title),
- last_read: get_relevant_last_read(username, e),
+ cr_entries = deep_titles
+ # For each Title, get the last read entry. If the user has finished
+ # reading this entry, get the next entry
+ .map { |t|
+ last_read_entry = t.entries.reverse_each.find do |e|
+ e.started? username
+ end
+ if last_read_entry && last_read_entry.finished? username
+ last_read_entry = last_read_entry.next_entry
+ end
+ last_read_entry
+ }
+ # Select elements with type `Entry` from the array and ignore all `Nil`s
+ .select(Entry)
+ .map { |e|
+ # Get the last read time of the entry. If it hasn't been started, get
+ # the last read time of the previous entry
+ last_read = e.load_last_read username
+ pe = e.previous_entry
+ if last_read.nil? && pe
+ last_read = pe.load_last_read username
+ end
+ {
+ entry: e,
+ percentage: e.load_percentage(username),
+ last_read: last_read,
+ }
}
- }
# Sort by by last_read, most recent first (nils at the end)
- continue_reading.sort! { |a, b|
+ cr_entries.sort { |a, b|
next 0 if a[:last_read].nil? && b[:last_read].nil?
next 1 if a[:last_read].nil?
next -1 if b[:last_read].nil?
@@ -533,65 +561,39 @@ class Library
}[0..11]
end
+ alias RA = NamedTuple(
+ entry: Entry,
+ percentage: Float64,
+ grouped_count: Int32)
+
def get_recently_added_entries(username)
- # Get all entries added within the last three months
- entries = titles.map { |t| t.entries }
+ recently_added = [] of RA
+
+ titles.map { |t| t.deep_entries }
.flatten
- .select { |e| e.date_added > 3.months.ago }
-
- # Group entries in a Hash by title ID
- grouped_entries = {} of String => Array(Entry)
- entries.each do |e|
- if grouped_entries.has_key? e.title_id
- grouped_entries[e.title_id].push e
- else
- grouped_entries[e.title_id] = [e]
+ .select { |e| e.date_added > 1.month.ago }
+ .sort { |a, b| b.date_added <=> a.date_added }
+ .each do |e|
+ last = recently_added.last?
+ if last && e.title_id == last[:entry].title_id &&
+ (e.date_added - last[:entry].date_added).duration < 1.day
+ # A NamedTuple is immutable, so we have to cast it to a Hash first
+ last_hash = last.to_h
+ count = last_hash[:grouped_count].as(Int32)
+ last_hash[:grouped_count] = count + 1
+ # Setting the percentage to a negative value will hide the
+ # percentage badge on the card
+ last_hash[:percentage] = -1.0
+ recently_added[recently_added.size - 1] = RA.from last_hash
+ else
+ recently_added << {
+ entry: e,
+ percentage: e.load_percentage(username),
+ grouped_count: 1,
+ }
+ end
end
- end
-
- # Cast the Hash to an Array of Tuples and sort it by date_added
- grouped_ary = grouped_entries.to_a.sort do |a, b|
- date_added_a = a[1].map { |e| e.date_added }.max
- date_added_b = b[1].map { |e| e.date_added }.max
- date_added_b <=> date_added_a
- end
-
- recently_added = grouped_ary.map do |_, ary|
- # Get the most recently added entry in the group
- entry = ary.sort { |a, b| a.date_added <=> b.date_added }.last
- {
- entry: entry,
- percentage: entry.book.load_percentage(username, entry.title),
- grouped_count: ary.size,
- }
- end
recently_added[0..11]
end
-
- private def get_continue_reading_entry(username, title)
- in_progress_entries = title.entries.select do |e|
- title.load_progress(username, e.title) > 0
- end
- return nil if in_progress_entries.empty?
-
- latest_read_entry = in_progress_entries[-1]
- if title.load_progress(username, latest_read_entry.title) ==
- latest_read_entry.pages
- title.next_entry latest_read_entry
- else
- latest_read_entry
- end
- end
-
- private def get_relevant_last_read(username, entry_obj)
- last_read = entry_obj.book.load_last_read username, entry_obj.title
- # grab from previous entry if current entry hasn't been started yet
- if last_read.nil?
- previous_entry = entry_obj.book.previous_entry(entry_obj)
- return entry_obj.book.load_last_read username, previous_entry.title \
- if previous_entry
- end
- last_read
- end
end
diff --git a/src/logger.cr b/src/logger.cr
index 8c049ed..b1d02a5 100644
--- a/src/logger.cr
+++ b/src/logger.cr
@@ -31,9 +31,9 @@ class Logger
{% end %}
@log = Log.for("")
-
@backend = Log::IOBackend.new
- @backend.formatter = ->(entry : Log::Entry, io : IO) do
+
+ format_proc = ->(entry : Log::Entry, io : IO) do
color = :default
{% begin %}
case entry.severity.label.to_s().downcase
@@ -50,12 +50,14 @@ class Logger
io << entry.message
end
- Log.builder.bind "*", @@severity, @backend
+ @backend.formatter = Log::Formatter.new &format_proc
+ Log.setup @@severity, @backend
end
# Ignores @@severity and always log msg
def log(msg)
- @backend.write Log::Entry.new "", Log::Severity::None, msg, nil
+ @backend.write Log::Entry.new "", Log::Severity::None, msg,
+ Log::Metadata.empty, nil
end
def self.log(msg)
diff --git a/src/mangadex/downloader.cr b/src/mangadex/downloader.cr
index 6a62e59..ba49de3 100644
--- a/src/mangadex/downloader.cr
+++ b/src/mangadex/downloader.cr
@@ -1,13 +1,13 @@
require "./api"
require "sqlite3"
-require "zip"
+require "compress/zip"
module MangaDex
class PageJob
property success = false
property url : String
property filename : String
- property writer : Zip::Writer
+ property writer : Compress::Zip::Writer
property tries_remaning : Int32
def initialize(@url, @filename, @writer, @tries_remaning)
@@ -324,7 +324,7 @@ module MangaDex
# Find the number of digits needed to store the number of pages
len = Math.log10(chapter.pages.size).to_i + 1
- writer = Zip::Writer.new zip_path
+ writer = Compress::Zip::Writer.new zip_path
# Create a buffered channel. It works as an FIFO queue
channel = Channel(PageJob).new chapter.pages.size
spawn do
diff --git a/src/mango.cr b/src/mango.cr
index d0484c9..ab814c0 100644
--- a/src/mango.cr
+++ b/src/mango.cr
@@ -4,7 +4,7 @@ require "./mangadex/*"
require "option_parser"
require "clim"
-MANGO_VERSION = "0.6.1"
+MANGO_VERSION = "0.7.0"
macro common_option
option "-c PATH", "--config=PATH", type: String,
diff --git a/src/routes/api.cr b/src/routes/api.cr
index 54bfb1b..4911c1b 100644
--- a/src/routes/api.cr
+++ b/src/routes/api.cr
@@ -80,7 +80,7 @@ class APIRouter < Router
if !entry_id.nil?
entry = title.get_entry(entry_id).not_nil!
raise "incorrect page value" if page < 0 || page > entry.pages
- title.save_progress username, entry.title, page
+ entry.save_progress username, page
elsif page == 0
title.unread_all username
else
@@ -224,7 +224,7 @@ class APIRouter < Router
entry_id = env.params.query["entry"]?
title = @context.library.get_title(title_id).not_nil!
- unless ["image/jpeg", "image/png"].includes? \
+ unless SUPPORTED_IMG_TYPES.includes? \
MIME.from_filename? filename
raise "The uploaded image must be either JPEG or PNG"
end
diff --git a/src/routes/main.cr b/src/routes/main.cr
index 8d2afec..6f28032 100644
--- a/src/routes/main.cr
+++ b/src/routes/main.cr
@@ -4,7 +4,7 @@ class MainRouter < Router
def initialize
get "/login" do |env|
base_url = Config.current.base_url
- render "src/views/login.ecr"
+ render "src/views/login.html.ecr"
end
get "/logout" do |env|
@@ -53,9 +53,8 @@ class MainRouter < Router
begin
title = (@context.library.get_title env.params.url["title"]).not_nil!
username = get_username env
- percentage = title.entries.map { |e|
- title.load_percentage username, e.title
- }
+ percentage = title.entries.map &.load_percentage username
+ title_percentage = title.titles.map &.load_percentage username
layout "title"
rescue e
@context.error e
diff --git a/src/routes/opds.cr b/src/routes/opds.cr
index 648bcac..567931e 100644
--- a/src/routes/opds.cr
+++ b/src/routes/opds.cr
@@ -4,13 +4,13 @@ class OPDSRouter < Router
def initialize
get "/opds" do |env|
titles = @context.library.titles
- render_xml "src/views/opds/index.ecr"
+ render_xml "src/views/opds/index.xml.ecr"
end
get "/opds/book/:title_id" do |env|
begin
title = @context.library.get_title(env.params.url["title_id"]).not_nil!
- render_xml "src/views/opds/title.ecr"
+ render_xml "src/views/opds/title.xml.ecr"
rescue e
@context.error e
env.response.status_code = 404
diff --git a/src/routes/reader.cr b/src/routes/reader.cr
index e9c32d3..4e3bc06 100644
--- a/src/routes/reader.cr
+++ b/src/routes/reader.cr
@@ -9,12 +9,15 @@ class ReaderRouter < Router
# load progress
username = get_username env
- page = title.load_progress username, entry.title
+ page = entry.load_progress username
# we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll
# library perloads a few pages in advance, and the user
# might not have actually read them
page = [page - 2 * IMGS_PER_PAGE, 1].max
+ # start from page 1 if the user has finished reading the entry
+ page = 1 if entry.finished? username
+
redirect env, "/reader/#{title.id}/#{entry.id}/#{page}"
rescue e
@context.error e
@@ -33,7 +36,7 @@ class ReaderRouter < Router
# save progress
username = get_username env
- title.save_progress username, entry.title, page
+ entry.save_progress username, page
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
urls = pages.map { |idx|
@@ -45,7 +48,7 @@ class ReaderRouter < Router
next_page = page + IMGS_PER_PAGE
next_url = next_entry_url = nil
exit_url = "#{base_url}book/#{title.id}"
- next_entry = title.next_entry entry
+ next_entry = entry.next_entry
unless next_page > entry.pages
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
end
@@ -53,7 +56,7 @@ class ReaderRouter < Router
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
end
- render "src/views/reader.ecr"
+ render "src/views/reader.html.ecr"
rescue e
@context.error e
env.response.status_code = 404
diff --git a/src/util.cr b/src/util.cr
index 921f281..c9dfa2b 100644
--- a/src/util.cr
+++ b/src/util.cr
@@ -16,11 +16,11 @@ macro layout(name)
is_admin = @context.storage.verify_admin token
end
page = {{name}}
- render "src/views/#{{{name}}}.ecr", "src/views/layout.ecr"
+ render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr"
rescue e
message = e.to_s
@context.error message
- render "src/views/message.ecr", "src/views/layout.ecr"
+ render "src/views/message.html.ecr", "src/views/layout.html.ecr"
end
end
@@ -139,7 +139,7 @@ macro render_xml(path)
end
macro render_component(filename)
- render "src/views/components/#{{{filename}}}.ecr"
+ render "src/views/components/#{{{filename}}}.html.ecr"
end
# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/
diff --git a/src/views/admin.ecr b/src/views/admin.html.ecr
similarity index 81%
rename from src/views/admin.ecr
rename to src/views/admin.html.ecr
index 9700fe8..b3e2d96 100644
--- a/src/views/admin.ecr
+++ b/src/views/admin.html.ecr
@@ -11,8 +11,9 @@
+Version: v<%= MANGO_VERSION %>
Log Out
<% content_for "script" do %>
-
-<% end %>
\ No newline at end of file
+
+<% end %>
diff --git a/src/views/components/card.ecr b/src/views/components/card.ecr
deleted file mode 100644
index 0eb07a5..0000000
--- a/src/views/components/card.ecr
+++ /dev/null
@@ -1,49 +0,0 @@
-<% if item.is_a? NamedTuple(entry: Entry, percentage: Float64, grouped_count: Int32) %>
-<% grouped_count = item[:grouped_count] %>
-<% if grouped_count == 1 %>
-<% item = item[:entry] %>
-<% else %>
-<% item = item[:entry].book %>
-<% end %>
-<% else %>
-<% grouped_count = 1 %>
-<% end %>
-
diff --git a/src/views/components/card.html.ecr b/src/views/components/card.html.ecr
new file mode 100644
index 0000000..88ddd5f
--- /dev/null
+++ b/src/views/components/card.html.ecr
@@ -0,0 +1,50 @@
+<% if item.is_a? NamedTuple(entry: Entry, percentage: Float64, grouped_count: Int32) %>
+ <% grouped_count = item[:grouped_count] %>
+ <% if grouped_count == 1 %>
+ <% item = item[:entry] %>
+ <% else %>
+ <% item = item[:entry].book %>
+ <% end %>
+<% else %>
+ <% grouped_count = 1 %>
+<% end %>
+
+
diff --git a/src/views/components/entry-modal.ecr b/src/views/components/entry-modal.html.ecr
similarity index 55%
rename from src/views/components/entry-modal.ecr
rename to src/views/components/entry-modal.html.ecr
index c064f4c..672e5c3 100644
--- a/src/views/components/entry-modal.ecr
+++ b/src/views/components/entry-modal.html.ecr
@@ -4,15 +4,16 @@
diff --git a/src/views/components/head.ecr b/src/views/components/head.html.ecr
similarity index 86%
rename from src/views/components/head.ecr
rename to src/views/components/head.html.ecr
index cbcbc07..399b907 100644
--- a/src/views/components/head.ecr
+++ b/src/views/components/head.html.ecr
@@ -4,7 +4,7 @@
Mango
-
+
diff --git a/src/views/components/sort-form.ecr b/src/views/components/sort-form.html.ecr
similarity index 56%
rename from src/views/components/sort-form.ecr
rename to src/views/components/sort-form.html.ecr
index 6036a15..93148d9 100644
--- a/src/views/components/sort-form.ecr
+++ b/src/views/components/sort-form.html.ecr
@@ -1,8 +1,8 @@
<% hash.each do |k, v| %>
- ▲ <%= v %>
- ▼ <%= v %>
+ ▲ <%= v %>
+ ▼ <%= v %>
<% end %>
diff --git a/src/views/download-manager.ecr b/src/views/download-manager.html.ecr
similarity index 74%
rename from src/views/download-manager.ecr
rename to src/views/download-manager.html.ecr
index e188438..0372010 100644
--- a/src/views/download-manager.ecr
+++ b/src/views/download-manager.html.ecr
@@ -23,10 +23,10 @@
<% content_for "script" do %>
-
-
-
-
-<% end %>
\ No newline at end of file
+
+
+
+
+<% end %>
diff --git a/src/views/download.ecr b/src/views/download.html.ecr
similarity index 88%
rename from src/views/download.ecr
rename to src/views/download.html.ecr
index d11c864..402c137 100644
--- a/src/views/download.ecr
+++ b/src/views/download.html.ecr
@@ -73,11 +73,11 @@
<% content_for "script" do %>
-
-
-
-
-
-<% end %>
\ No newline at end of file
+
+
+
+
+
+<% end %>
diff --git a/src/views/home.ecr b/src/views/home.ecr
deleted file mode 100644
index 04f1bb8..0000000
--- a/src/views/home.ecr
+++ /dev/null
@@ -1,69 +0,0 @@
-<%- if new_user && empty_library -%>
-
-
-
-
Add your first manga
-
We can't find any files yet. Add some to your library and they'll appear here.
-
- Current library path
- <%= Config.current.library_path %>
- Want to change your library path?
- Update config.yml
located at: <%= Config.current.path %>
- Can't see your files yet?
- You must wait <%= Config.current.scan_interval %> minutes for the library scan to complete
- <% if is_admin %>, or manually re-scan from Admin <% end %>.
-
-
-
-<%- elsif new_user && empty_library == false -%>
-
-
-
-
Read your first manga
-
Once you start reading, Mango will remember where you left off
- and show your entries here.
-
View library
-
-
-<%- elsif new_user == false && empty_library == false -%>
-
-<%- if continue_reading.empty? && recently_added.empty? -%>
-
-
-
A self-hosted manga server and reader
-
View library
-
-<%- end -%>
-
-<%- unless continue_reading.empty? -%>
-Continue Reading
-
- <%- continue_reading.each do |cr| -%>
- <% item = cr[:entry] %>
- <% progress = cr[:percentage] %>
- <%= render_component "card" %>
- <%- end -%>
-
-<%- end -%>
-
-<%- unless recently_added.empty? -%>
-Recently Added
-
- <%- recently_added.each do |ra| -%>
- <% item = ra %>
- <% progress = ra[:percentage] %>
- <%= render_component "card" %>
- <%- end -%>
-
-<%- end -%>
-
-<%= render_component "entry-modal" %>
-
-<%- end -%>
-
-<% content_for "script" do %>
-
-
-
-
-<% end %>
diff --git a/src/views/home.html.ecr b/src/views/home.html.ecr
new file mode 100644
index 0000000..cb0f53e
--- /dev/null
+++ b/src/views/home.html.ecr
@@ -0,0 +1,73 @@
+<%- if new_user && empty_library -%>
+
+
+
+
Add your first manga
+
We can't find any files yet. Add some to your library and they'll appear here.
+
+ Current library path
+ <%= Config.current.library_path %>
+ Want to change your library path?
+ Update config.yml
located at: <%= Config.current.path %>
+ Can't see your files yet?
+
+ You must wait <%= Config.current.scan_interval %> minutes for the library scan to complete
+ <% if is_admin %>
+ , or manually re-scan from Admin
+ <% end %>.
+
+
+
+
+<%- elsif new_user && empty_library == false -%>
+
+
+
+
Read your first manga
+
Once you start reading, Mango will remember where you left off
+ and show your entries here.
+
View library
+
+
+<%- elsif new_user == false && empty_library == false -%>
+
+ <%- if continue_reading.empty? && recently_added.empty? -%>
+
+
+
A self-hosted manga server and reader
+
View library
+
+ <%- end -%>
+
+ <%- unless continue_reading.empty? -%>
+ Continue Reading
+
+ <%- continue_reading.each do |cr| -%>
+ <% item = cr[:entry] %>
+ <% progress = cr[:percentage] %>
+ <%= render_component "card" %>
+ <%- end -%>
+
+ <%- end -%>
+
+ <%- unless recently_added.empty? -%>
+ Recently Added
+
+ <%- recently_added.each do |ra| -%>
+ <% item = ra %>
+ <% progress = ra[:percentage] %>
+ <%= render_component "card" %>
+ <%- end -%>
+
+ <%- end -%>
+
+ <%= render_component "entry-modal" %>
+
+<%- end -%>
+
+<% content_for "script" do %>
+
+
+
+
+<% end %>
diff --git a/src/views/layout.ecr b/src/views/layout.ecr
deleted file mode 100644
index 8242be5..0000000
--- a/src/views/layout.ecr
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-<%= render_component "head" %>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <%= yield_content "script" %>
-
-
-
diff --git a/src/views/layout.html.ecr b/src/views/layout.html.ecr
new file mode 100644
index 0000000..2800ddc
--- /dev/null
+++ b/src/views/layout.html.ecr
@@ -0,0 +1,68 @@
+
+
+
+ <%= render_component "head" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= yield_content "script" %>
+
+
+
diff --git a/src/views/library.ecr b/src/views/library.html.ecr
similarity index 69%
rename from src/views/library.ecr
rename to src/views/library.html.ecr
index afbbec5..2550f1e 100644
--- a/src/views/library.ecr
+++ b/src/views/library.html.ecr
@@ -18,14 +18,14 @@
<% titles.each_with_index do |item, i| %>
- <% progress = percentage[i] %>
- <%= render_component "card" %>
+ <% progress = percentage[i] %>
+ <%= render_component "card" %>
<% end %>
<% content_for "script" do %>
-
-
-
-
+
+
+
+
<% end %>
diff --git a/src/views/login.ecr b/src/views/login.ecr
deleted file mode 100644
index 264395b..0000000
--- a/src/views/login.ecr
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-<%= render_component "head" %>
-
-
-
-
-
-
-
-
-
diff --git a/src/views/login.html.ecr b/src/views/login.html.ecr
new file mode 100644
index 0000000..1bc122a
--- /dev/null
+++ b/src/views/login.html.ecr
@@ -0,0 +1,36 @@
+
+
+
+ <%= render_component "head" %>
+
+
+
+
+
+
+
+
+
diff --git a/src/views/message.ecr b/src/views/message.ecr
deleted file mode 100644
index 7546751..0000000
--- a/src/views/message.ecr
+++ /dev/null
@@ -1 +0,0 @@
-<%= message %>
\ No newline at end of file
diff --git a/src/views/message.html.ecr b/src/views/message.html.ecr
new file mode 100644
index 0000000..1a5b8d8
--- /dev/null
+++ b/src/views/message.html.ecr
@@ -0,0 +1 @@
+<%= message %>
diff --git a/src/views/opds/index.ecr b/src/views/opds/index.xml.ecr
similarity index 67%
rename from src/views/opds/index.ecr
rename to src/views/opds/index.xml.ecr
index 16fe193..2d84b63 100644
--- a/src/views/opds/index.ecr
+++ b/src/views/opds/index.xml.ecr
@@ -13,10 +13,10 @@
<% titles.each do |t| %>
-
- <%= t.display_name %>
- urn:mango:<%= t.id %>
-
-
+
+ <%= t.display_name %>
+ urn:mango:<%= t.id %>
+
+
<% end %>
diff --git a/src/views/opds/title.ecr b/src/views/opds/title.ecr
deleted file mode 100644
index 476bb8d..0000000
--- a/src/views/opds/title.ecr
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
- urn:mango:<%= title.id %>
-
-
-
-
- <%= title.display_name %>
-
-
- Mango
- https://github.com/hkalexling/Mango
-
-
- <% title.titles.each do |t| %>
-
- <%= t.display_name %>
- urn:mango:<%= t.id %>
-
-
- <% end %>
-
- <% title.entries.each do |e| %>
-
- <%= e.display_name %>
- urn:mango:<%= e.id %>
-
-
-
-
-
-
-
-
-
- <% end %>
-
-
diff --git a/src/views/opds/title.xml.ecr b/src/views/opds/title.xml.ecr
new file mode 100644
index 0000000..80eadfa
--- /dev/null
+++ b/src/views/opds/title.xml.ecr
@@ -0,0 +1,38 @@
+
+
+ urn:mango:<%= title.id %>
+
+
+
+
+ <%= title.display_name %>
+
+
+ Mango
+ https://github.com/hkalexling/Mango
+
+
+ <% title.titles.each do |t| %>
+
+ <%= t.display_name %>
+ urn:mango:<%= t.id %>
+
+
+ <% end %>
+
+ <% title.entries.each do |e| %>
+
+ <%= e.display_name %>
+ urn:mango:<%= e.id %>
+
+
+
+
+
+
+
+
+
+ <% end %>
+
+
diff --git a/src/views/reader.ecr b/src/views/reader.ecr
deleted file mode 100644
index d62513c..0000000
--- a/src/views/reader.ecr
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-<%= render_component "head" %>
-
-
-
-
-
- <%- urls.each_with_index do |url, i| -%>
-
- <%- end -%>
- <%- if next_url -%>
-
- <%- end -%>
-
- <%- if next_entry_url -%>
-
Next Entry
- <%- else -%>
-
Exit Reader
- <%- end -%>
-
-
-
-
-
-
-
-
-
-
-
-
Jump to page
-
-
- <%- (1..entry.pages).each do |p| -%>
- <%= p %>
- <%- end -%>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/reader.html.ecr b/src/views/reader.html.ecr
new file mode 100644
index 0000000..f791163
--- /dev/null
+++ b/src/views/reader.html.ecr
@@ -0,0 +1,62 @@
+
+
+
+ <%= render_component "head" %>
+
+
+
+
+ <%- urls.each_with_index do |url, i| -%>
+
+ <%- end -%>
+ <%- if next_url -%>
+
+ <%- end -%>
+
+ <%- if next_entry_url -%>
+
Next Entry
+ <%- else -%>
+
Exit Reader
+ <%- end -%>
+
+
+
+
+
+
+
+
+
+
+
+
Jump to page
+
+
+ <%- (1..entry.pages).each do |p| -%>
+ <%= p %>
+ <%- end -%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/title.ecr b/src/views/title.html.ecr
similarity index 73%
rename from src/views/title.ecr
rename to src/views/title.html.ecr
index d6f70b0..b926a5a 100644
--- a/src/views/title.ecr
+++ b/src/views/title.html.ecr
@@ -2,14 +2,14 @@
<%= title.display_name %>
<% if is_admin %>
-
+
<% end %>
@@ -33,12 +33,12 @@
<% title.titles.each_with_index do |item, i| %>
- <% progress = nil %>
- <%= render_component "card" %>
+ <% progress = title_percentage[i] %>
+ <%= render_component "card" %>
<% end %>
<% title.entries.each_with_index do |item, i| %>
- <% progress = percentage[i] %>
- <%= render_component "card" %>
+ <% progress = percentage[i] %>
+ <%= render_component "card" %>
<% end %>
@@ -72,7 +72,7 @@
Upload a cover image by dropping it here or
-
+ ">
selecting one
@@ -85,8 +85,8 @@
Progress
- Mark all as read (100%)
- Mark all as unread (0%)
+ Mark all as read (100%)
+ Mark all as unread (0%)
@@ -94,10 +94,10 @@
<% content_for "script" do %>
-
-
-
-
-
-
+
+
+
+
+
+
<% end %>
diff --git a/src/views/user-edit.ecr b/src/views/user-edit.ecr
deleted file mode 100644
index 9b354d2..0000000
--- a/src/views/user-edit.ecr
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-<% content_for "script" do %>
-
-
-
-<% end %>
\ No newline at end of file
diff --git a/src/views/user-edit.html.ecr b/src/views/user-edit.html.ecr
new file mode 100644
index 0000000..22916de
--- /dev/null
+++ b/src/views/user-edit.html.ecr
@@ -0,0 +1,46 @@
+
+
+<% content_for "script" do %>
+
+
+
+<% end %>
diff --git a/src/views/user.ecr b/src/views/user.ecr
deleted file mode 100644
index 3887d2a..0000000
--- a/src/views/user.ecr
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Username
- Admin Access
- Actions
-
-
-
- <%- users.each do |u| -%>
-
- <%= u[0] %>
- <%= u[1] %>
-
-
- <%- if u[0] != username %>
-
- <%- end %>
-
-
- <%- end -%>
-
-
-
-New User
-
-
-<% content_for "script" do %>
-
-
-<% end %>
\ No newline at end of file
diff --git a/src/views/user.html.ecr b/src/views/user.html.ecr
new file mode 100644
index 0000000..c65dbb7
--- /dev/null
+++ b/src/views/user.html.ecr
@@ -0,0 +1,31 @@
+
+
+
+ Username
+ Admin Access
+ Actions
+
+
+
+ <%- users.each do |u| -%>
+
+ <%= u[0] %>
+ <%= u[1] %>
+
+
+ <%- if u[0] != username %>
+
+ <%- end %>
+
+
+ <%- end -%>
+
+
+
+New User
+
+
+<% content_for "script" do %>
+
+
+<% end %>