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 %> -
- id="<%= item.id %>" - <% end %>> - - - href="<%= base_url %>book/<%= item.id %>" - <% end %>> - -
- onclick="showModal("<%= item.encoded_path %>", '<%= item.pages %>', <%= (progress.not_nil! * 100).round(1) %>, "<%= item.book.encoded_display_name %>", "<%= item.encoded_display_name %>", '<%= item.title_id %>', '<%= item.id %>')" - <% end %>> - -
- -
- -
- <% unless (item.is_a? Title && item.entries.size == 0) || progress.nil? %> -
<%= (progress * 100).round(1) %>%
- <% end %> - -

"><%= item.display_name %>

- <% if item.is_a? Entry %> -

<%= item.pages %> pages

- <% end %> - <% if item.is_a? Title %> - <% if grouped_count == 1 %> -

<%= item.size %> entries

- <% else %> -

<%= grouped_count %> new entries

- <% end %> - <% 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 %> + +
+ id="<%= item.id %>" + <% end %>> + + + href="<%= base_url %>book/<%= item.id %>" + <% end %>> + +
+ onclick="showModal("<%= item.encoded_path %>", '<%= item.pages %>', <%= (progress * 100).round(1) %>, "<%= item.book.encoded_display_name %>", "<%= item.encoded_display_name %>", '<%= item.title_id %>', '<%= item.id %>')" + <% end %>> + +
+ +
+ +
+ <% unless progress < 0 || progress > 100 %> +
<%= (progress * 100).round(1) %>%
+ <% end %> + +

"><%= item.display_name %>

+ <% if item.is_a? Entry %> +

<%= item.pages %> pages

+ <% end %> + <% if item.is_a? Title %> + <% if grouped_count == 1 %> +

<%= item.size %> entries

+ <% else %> +

<%= grouped_count %> new entries

+ <% end %> + <% 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 @@
<% if page == "home" %> -

+

<% end %>

- <% unless page == "home" %>   - <% if is_admin %> - - <% end %> + <% unless page == "home" %> + <% if is_admin %> + + <% end %> <% end %> +

@@ -21,13 +22,13 @@

Read

- From beginning - + From beginning +

Progress

- - + +

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 @@
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" %> - - -
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
-
-
- <%= content %> -
-
- - - - - <%= 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" %> + + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+ <%= content %> +
+
+ + + + + <%= 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" %> - - -
-
-
-
-
-
-

Log In

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - 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" %> + + +
+
+
+
+
+
+

Log In

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + 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 -%> - - <%- else -%> - - <%- 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 -%> + + <%- else -%> + + <%- 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 @@ @@ -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 @@ -
- -
- - value=<%= username %> <%- end -%>> -
- <%- if new_user -%> -
- - -
- <%- end -%> -
- - checked <%- end -%>> -
- - <%- if !new_user -%> -
- - -
- <%- end -%> - -
- - -
- -<% 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 @@ +
+ +
+ + value=<%= username %> <%- end -%>> +
+ <%- if new_user -%> +
+ + +
+ <%- end -%> +
+ + checked <%- end -%>> +
+ + <%- unless new_user -%> +
+ + +
+ <%- end -%> + +
+ + +
+ +<% 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 @@ - - - - - - - - - - <%- users.each do |u| -%> - - - - - - <%- end -%> - -
UsernameAdmin AccessActions
<%= u[0] %><%= u[1] %> - - <%- if u[0] != username %> - - <%- 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 @@ + + + + + + + + + + <%- users.each do |u| -%> + + + + + + <%- end -%> + +
UsernameAdmin AccessActions
<%= u[0] %><%= u[1] %> + + <%- if u[0] != username %> + + <%- end %> +
+ +New User + + +<% content_for "script" do %> + + +<% end %>