Compare commits

...

480 Commits

Author SHA1 Message Date
Alex Ling 40f74ea375 Merge pull request #153 from hkalexling/hotfix/reader-bg
Fix incorrect background color on reader page
2021-01-27 15:19:04 +08:00
Alex Ling adf260bc35 Bump version to v0.19.1 2021-01-27 06:33:45 +00:00
Alex Ling 432d6f0cd5 Run CI for hotfix/* branches 2021-01-27 06:33:45 +00:00
Alex Ling 3de314ae9a Fix incorrect background color on reader page 2021-01-27 06:33:45 +00:00
Alex Ling 952aa0c6ca Fix linter 2021-01-17 15:59:42 +00:00
Alex Ling bd81c2e005 Fix incorrect migration SQL 2021-01-17 15:58:13 +00:00
Alex Ling b471ed2fa0 Upgrade MG 2021-01-17 15:49:10 +00:00
Alex Ling 7507ab64ad Bump version to v0.19.0 2021-01-17 08:34:35 +00:00
Alex Ling e4587d36bc Fix linter 2021-01-17 08:25:01 +00:00
Alex Ling 7d6d3640ad Disable the tagging UI for non-admin users 2021-01-17 08:16:40 +00:00
Alex Ling 3071d44e32 Fix admin API bypassing 2021-01-17 08:10:43 +00:00
Alex Ling 7a09c9006a Set up foreign keys 2021-01-17 04:47:06 +00:00
Alex Ling 959560c7a7 Add titles and move insert_ids to class variable
This fixes the bug where the new ids are not saved
2021-01-17 04:45:55 +00:00
Alex Ling ff679b30d8 Capitalize the UNIQUE keyword 2021-01-17 04:41:05 +00:00
Alex Ling f7a360c2d8 Proper DB migration 2021-01-16 17:11:57 +00:00
Alex Ling 1065b430e3 Rewrite tagging UI with suggestions (#146) 2021-01-14 13:08:50 +00:00
Alex Ling 5abf7032a5 Use less 2021-01-14 13:04:57 +00:00
Alex Ling 44336c546a Bump version to v0.18.3 2021-01-12 10:14:12 +00:00
Alex Ling a4c6e6611c Try WSS first, and fallback to WS (#144) 2021-01-12 10:13:06 +00:00
Alex Ling 0b457a2797 Merge branch 'master' of https://github.com/hkalexling/Mango 2021-01-11 15:37:34 +00:00
Alex Ling 653751bede Merge branch 'dev' 2021-01-11 15:37:06 +00:00
Alex Ling a02bf4a81e Bump version to v0.18.2 2021-01-11 15:22:51 +00:00
Alex Ling 5271d12f4c Respect base URL in WS connections 2021-01-11 15:05:58 +00:00
Alex Ling c2e2f0b9b3 Merge pull request #143 from hkalexling/all-contributors/add-h45h74x
docs: add h45h74x as a contributor
2021-01-11 19:32:47 +08:00
allcontributors[bot] 72d319902e docs: update .all-contributorsrc [skip ci] 2021-01-11 11:31:21 +00:00
allcontributors[bot] bbd0fd68cb docs: update README.md [skip ci] 2021-01-11 11:31:20 +00:00
Alex Ling 0fb1e1598d Remove sourcerer.io HoF and use all-contributors
[skip ci]
RIP sourcerer.io https://github.com/sourcerer-io/sourcerer-app/issues/632
2021-01-11 11:28:30 +00:00
Alex Ling 4645582f5d Bump version to v0.18.1 2021-01-11 05:29:28 +00:00
Alex Ling ac9c51dd33 Remove non-existing #root from css selectors (#142) 2021-01-11 05:28:44 +00:00
Alex Ling f51d27860a Validate input index before flipping page 2021-01-09 15:49:34 +00:00
Alex Ling 4a7439a1ea Merge branch 'dev' of https://github.com/hkalexling/Mango into dev 2021-01-09 06:40:49 +00:00
Alex Ling 00e19399d7 Check login is disabled before accessing default username 2021-01-09 06:35:26 +00:00
Alex Ling cb723acef7 Update config in README 2021-01-09 06:35:11 +00:00
Alex Ling 794bed12bd Merge pull request #139 from h45h74x/feature/plugin-helper-function-post
Added post helper function
2021-01-09 14:30:52 +08:00
Simon bae8220e75 Added post helper function 2021-01-08 21:17:58 +01:00
Alex Ling 0cc5e1626b Fix broken buttons on download manager page 2021-01-08 11:38:51 +00:00
Alex Ling da0ca665a6 Mark entry as read when exiting reader at the end 2021-01-08 11:38:25 +00:00
Alex Ling a91cf21aa9 Bump version to v0.18.0 2021-01-07 16:27:22 +00:00
Alex Ling 39b2636711 Sort tags in title 2021-01-07 16:21:23 +00:00
Alex Ling 2618d8412b Update the API doc to include margin in dimensions 2021-01-07 16:06:43 +00:00
Alex Ling 445ebdf357 Merge pull request #136 from h45h74x/feature/adjustable-page-gaps
Feature/adjustable page gaps
2021-01-07 01:11:34 +08:00
Simon 60134dc364 Formatting 2021-01-06 17:44:02 +01:00
Simon aa70752244 Moved margin value to the dimensions API 2021-01-06 17:30:55 +01:00
Simon 0f39535097 Added new entry in example config 2021-01-06 15:28:09 +01:00
Simon e086bec9da Added adjustable page gaps via config 2021-01-06 15:27:48 +01:00
Alex Ling dcdcf29114 Sort tags on the tags page 2021-01-05 07:34:31 +00:00
Alex Ling c5c73ddff3 Rewrite download-manager.js 2021-01-01 09:19:16 +00:00
Alex Ling f18ee4284f Rewrite admin.js with Alpine component 2021-01-01 09:04:53 +00:00
Alex Ling 0fbc11386e Fix broken "Exit Reader" button 2021-01-01 09:04:18 +00:00
Alex Ling a68282b4bf Rewrite reader.js with a reusable alpine function 2020-12-31 16:21:00 +00:00
Alex Ling e64908ad06 Remove the outdated styleModal call 2020-12-31 14:08:14 +00:00
Alex Ling af0913df64 Dynamic HTML title 2020-12-31 14:08:14 +00:00
Alex Ling 5685dd1cc5 Use tallboy to draw CLI table 2020-12-30 16:44:23 +00:00
Alex Ling af2fd2a66a Remove the Context and Router classes 2020-12-30 15:58:51 +00:00
Alex Ling db2a51a26b Clean up library classes 2020-12-30 15:23:38 +00:00
Alex Ling cf930418cb Update rename spec 2020-12-30 12:53:48 +00:00
Alex Ling 911848ad11 Merge branch 'feature/tagging' into dev 2020-12-30 11:15:44 +00:00
Alex Ling 93f745aecb Only admins can add or delete tags 2020-12-30 11:13:43 +00:00
Alex Ling 981a1f0226 Add /tags to nav bar 2020-12-30 11:13:43 +00:00
Alex Ling 8188456788 Finish tagging 2020-12-30 11:13:43 +00:00
Alex Ling 1eace2c64c Add the /tags/:tag page 2020-12-30 11:13:43 +00:00
Alex Ling c6ee5409f8 Trim input tag 2020-12-30 11:13:43 +00:00
Alex Ling b05ed57762 Add API endpoints for tags 2020-12-30 11:13:43 +00:00
Alex Ling 0f1d1099f6 Add unique constraint to tags and error handling 2020-12-30 11:13:43 +00:00
Alex Ling 40a24f4247 Add tags to the web UI 2020-12-30 11:13:43 +00:00
Alex Ling a6862e86d4 Update alpine 2020-12-30 11:13:43 +00:00
Alex Ling bfc1b697bd Add tag related methods for Title 2020-12-30 11:13:43 +00:00
Alex Ling 276f62cb76 Update DB for tags 2020-12-30 11:13:43 +00:00
Alex Ling 45a81ad5f6 Display the entries and sub-titles count 2020-12-30 11:13:43 +00:00
Alex Ling ce88acb9e5 Simplify the request_path_startswith helper method 2020-12-30 11:13:43 +00:00
Alex Ling bd34b803f1 Tokens take precedence over default user setting 2020-12-30 11:13:43 +00:00
Alex Ling 2559f65f35 Display the entries and sub-titles count 2020-12-29 04:33:55 +00:00
Alex Ling 93c21ea659 Simplify the request_path_startswith helper method 2020-12-28 16:29:29 +00:00
Alex Ling 85ad38c321 Allow disable login 2020-12-28 16:13:51 +00:00
Alex Ling b6a204f5bd Escape illegal filename characters in Windows 2020-12-28 15:20:09 +00:00
Alex Ling f7b8e2d852 Bump version to v0.17.1 2020-12-27 09:46:14 +00:00
Alex Ling 946017c8bd Fix function redeclaration 2020-12-27 09:42:06 +00:00
Alex Ling ec5256dabd Improve batch mark UX (#97) 2020-12-27 09:42:06 +00:00
Alex Ling 4e707076a1 By default use the system theme setting (#111) 2020-12-27 09:42:06 +00:00
Alex Ling 66a3cc268b Merge branch 'master' into dev 2020-12-26 09:34:23 +00:00
Alex Ling 96949905b9 Cache entry display names
This improves the title page load time (#116)
2020-12-26 09:32:03 +00:00
Alex Ling 30c0199039 Merge branch 'dev' 2020-12-26 07:33:04 +00:00
Alex Ling 7a7cb78f82 Check bool environment variables are "1" or "true" 2020-12-26 07:11:10 +00:00
Alex Ling 8931ba8c43 Bump version to 0.17.0 2020-12-26 04:02:58 +00:00
Alex Ling d50981c151 Fix typos 2020-12-26 04:01:57 +00:00
Alex Ling df4deb1415 Allow proxy with authentication 2020-12-22 02:32:07 +00:00
Alex Ling aa5e999ed4 Allow users to disable SSL verification 2020-12-21 06:04:26 +00:00
Alex Ling 84d4b0c529 Switch to redoc and update API description 2020-12-21 06:04:26 +00:00
Alex Ling d3e5691478 Add overall description of the API 2020-12-14 15:20:50 +00:00
Alex Ling 1000b02ae0 Exclude /src/routes/api.cr from line width check 2020-12-14 14:59:18 +00:00
Alex Ling 1f795889a9 Move the entry download route to API 2020-12-14 13:03:23 +00:00
Alex Ling d33b45233a Use the correct verbs in the API 2020-12-14 12:49:56 +00:00
Alex Ling 4f6df5b9a3 Rename bulk-progress to bulk_progress 2020-12-14 11:54:25 +00:00
Alex Ling 341b586cb3 Add API documentation 2020-12-14 11:09:38 +00:00
Alex Ling 9dcc9665ce Cancel a download job when deleted from web UI 2020-12-12 16:15:16 +00:00
Alex Ling 1cd90926df Bind boolean attributes 2020-12-11 10:22:08 +00:00
Alex Ling ac1ff61e6d Move theme.js to common.js
This reduces the number of JS files to include when loading
2020-12-11 10:11:39 +00:00
Alex Ling 6ea41f79e9 Simplify the showControl calls on reader page 2020-12-11 09:47:32 +00:00
Alex Ling dad02a2a30 Move getProp and setProp to common.js 2020-12-11 09:46:56 +00:00
Alex Ling 280490fb36 Rewrite the download manager page 2020-12-11 07:46:47 +00:00
Alex Ling 455315a362 Upgrade to Crystal 0.35.1 2020-12-11 07:46:47 +00:00
Alex Ling df51406638 Use $GITHUB_ENV [skip ci] 2020-11-24 13:43:54 +08:00
Alex Ling 531d42ef18 [skip ci] enable set-env
https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
2020-11-24 13:38:15 +08:00
Alex Ling 2645e8cd05 Merge branch 'dev' 2020-11-24 05:31:06 +00:00
Alex Ling b2dc44a919 Reverse J and K for page navigation 2020-11-24 05:09:06 +00:00
Alex Ling c8db397a3b Bump version to v0.16.0 2020-11-24 04:30:47 +00:00
Alex Ling 6384d4b77a Log "DB optimization finished" in the info level 2020-11-24 04:05:07 +00:00
Alex Ling 1039732d87 Log the full file path in error messages (#123) 2020-11-24 04:03:53 +00:00
Alex Ling 011123f690 Allow keyboard navigation on reader page (#124) 2020-11-24 03:57:38 +00:00
Alex Ling e602a35b0c Merge branch 'dev' 2020-11-02 16:32:08 +00:00
Alex Ling 7792d3426e Bump version to v0.15.1 2020-11-01 09:22:05 +00:00
Alex Ling b59c8f85ad Fix scroller issues in continuous reader (#121) 2020-10-31 04:29:46 +00:00
Alex Ling 18834ac28e Set thumbnail size and mimetype 2020-10-29 04:06:44 +00:00
Alex Ling bf68e32ac8 Merge branch 'dev' 2020-10-25 07:57:26 +00:00
Alex Ling 54eb041fe4 Update README 2020-10-25 07:29:19 +00:00
Alex Ling 57d8c100f9 Bump version to v0.15.0 2020-10-25 07:22:38 +00:00
Alex Ling 56d973b99d Get progress when page loads and when post 2020-10-25 07:21:08 +00:00
Alex Ling 670e5cdf6a Better logging when optimizing DB 2020-10-25 07:09:37 +00:00
Alex Ling 1b35392f9c Remove unnecessary properties 2020-10-25 07:09:21 +00:00
Alex Ling c4e1ffe023 Trigger thumbnail generation from the admin page 2020-10-25 05:41:27 +00:00
Alex Ling 44f4959477 Finish thumbnail generation and DB optimization
(#93)
2020-10-24 04:13:11 +00:00
Alex Ling 0582b57d60 Add config options for optimization tasks 2020-10-24 03:50:26 +00:00
Alex Ling 83d96fd2a1 Add the route to serve thumbnails 2020-10-23 12:30:47 +00:00
Alex Ling 8ac89c420c Add helper methods for thumbnail generation 2020-10-23 12:30:29 +00:00
Alex Ling 968c2f4ad5 Update DB to save thumbnails 2020-10-23 12:29:20 +00:00
Alex Ling ad940f30d5 Update image_size.cr to 0.4.0 for better err msg 2020-10-23 12:21:05 +00:00
Alex Ling 308ad4e063 Only truncate visible titles to improve load time 2020-10-20 14:36:56 +00:00
Alex Ling 4d709b7eb5 Update default config in README 2020-10-18 12:53:43 +00:00
Alex Ling 5760ad924e Bump version to v0.14.0 2020-10-18 12:22:26 +00:00
Alex Ling fff171c8c9 Bump version to v0.13.0 2020-10-18 11:39:24 +00:00
Alex Ling 44ff566a1d Merge branch 'feature/paged-reader' into dev 2020-10-15 11:52:15 +00:00
Alex Ling 853f422964 Configurable read timeout (#108) 2020-10-15 11:51:04 +00:00
Alex Ling 3bb0917374 Allow /manga/<id> URL for MangaDex 2020-10-15 11:38:22 +00:00
Alex Ling a86f0d0f34 Add paged reading mode 2020-10-09 10:09:42 +00:00
Alex Ling 16a9d7fc2e Merge pull request #110 from XavierSchiller/master
[arm64] Fix Wrong libgc.so location when building Image
2020-09-27 20:07:44 +08:00
Xavier ee2b4abc85 Fix Wrong libgc.so location when building Image.
The Repo Maintainer was using the location of libgc.so from the armhf package, however, according to:
https://debian.pkgs.org/9/debian-main-arm64/libgc-dev_7.4.2-8_arm64.deb.html and
https://packages.ubuntu.com/focal/arm64/libgc-dev/filelist
it exists under /usr/lib/aarch64-linux-gnu/
2020-09-27 14:35:43 +05:30
Alex Ling a6c2799521 Bump version to v0.12.3 2020-09-22 08:55:29 +00:00
Alex Ling 2370e4d2c6 Add browserstack as a sponsor 2020-09-22 08:54:20 +00:00
Alex Ling 32b0384ea0 Clearer gulpfile 2020-09-22 08:46:53 +00:00
Alex Ling 50d4ffdb7b Use babel and polyfill.io 2020-09-22 07:40:47 +00:00
Alex Ling 96463641f9 Update progress on last page (#105) 2020-09-21 04:35:23 +00:00
Alex Ling ddbba5d596 Bump version to v0.12.2 2020-09-17 16:08:52 +00:00
Alex Ling 2a04f4531e Bound the page number in the reader route
fixes #104
2020-09-17 16:06:01 +00:00
Alex Ling a5b6fb781f Bump version to v0.12.1 2020-09-17 13:32:00 +00:00
Alex Ling 8dfdab9d73 Respect the base URL in direct download link (#103) 2020-09-17 13:29:52 +00:00
Alex Ling 3a95270dfb Don't copy unused UIKit files 2020-09-17 13:25:35 +00:00
Alex Ling 2960ca54df Move fontawesome to NPM 2020-09-17 13:20:24 +00:00
Alex Ling f5fe3c6b1c Use image_size.cr v0.2.0 2020-09-16 15:40:01 +00:00
Alex Ling a612cc15fb Bump version to v0.12.0 2020-09-12 14:35:56 +00:00
Alex Ling c9c0818069 Add inline documentation to reader.js 2020-09-12 14:32:29 +00:00
Alex Ling 2f8efc382f Clean up 2020-09-12 14:05:15 +00:00
Alex Ling a0fb1880bd Update Dockerfile [skip ci] 2020-09-12 13:59:32 +00:00
Alex Ling a408f14425 Add .dockerignore 2020-09-12 13:59:14 +00:00
Alex Ling 243b6c8927 Typo fix [skip ci] 2020-09-12 07:13:15 +00:00
Alex Ling ff3a44d017 Update ARM dockerfiles to use image_size.cr 2020-09-12 07:03:30 +00:00
Alex Ling 67ef1f7112 DRY when listing archive entries 2020-09-12 06:58:03 +00:00
Alex Ling 5d7b8a1ef9 Skip error entries in OPDS feed 2020-09-12 06:57:42 +00:00
Alex Ling a68f3eea95 Allow hyphens in username (#99) 2020-09-12 05:29:25 +00:00
Alex Ling 220fc42bf2 Add system dependencies for image_size.cr 2020-09-11 17:02:45 +00:00
Alex Ling a45e6ea3da Rewrite web reader 2020-09-11 17:00:42 +00:00
Alex Ling 88394d4636 Expose page ratios through API 2020-09-11 17:00:28 +00:00
Alex Ling ef1ab940f5 Fix GitHub tags of dependencies in the Dockerfiles
[skip ci]
2020-08-16 16:45:57 +00:00
Alex Ling 97a1c408d8 Bump version to v0.11.0 2020-08-16 12:44:02 +00:00
Alex Ling abbf77df13 Merge branch 'master' into dev 2020-08-10 14:32:53 +00:00
Alex Ling 3b4021f680 Workflow retry hack
I got random "Invalid memory access" when running `crystal build`.
This is probably a compiler or LLVM bug.
We use this temporary hack to retry until they fix it.
2020-08-10 13:14:05 +00:00
Alex Ling 68b1923cb6 Clear title ID at the end of scans
This minimizes the chance of getting an unexpected empty home page
2020-08-10 11:45:50 +00:00
Alex Ling 3cdd4b29a5 Add back to top button to all pages (#95) 2020-08-10 11:42:23 +00:00
Alex Ling af84c0f6de Fix typo 2020-08-08 17:04:42 +08:00
Alex Ling 85a65f84d0 Remove unnecessary "require" statements 2020-08-06 18:10:13 +00:00
Alex Ling 5027a911cd Respect the *_PROXY environment variables (#94) 2020-08-06 17:01:53 +00:00
Alex Ling ac63bf7599 Add sponsors [skip ci] 2020-08-06 12:49:43 +08:00
Alex Ling 30b0e0b8fb Pin down mythml and duktape versions in Dockerfile
[skip ci]
2020-08-05 12:00:49 +00:00
Alex Ling ddda058d8d Fix spec 2020-08-05 09:59:52 +00:00
Alex Ling 46db25e8e0 Fix wildcard in workflow 2020-08-05 09:50:46 +00:00
Alex Ling c07f421322 Fix CLI tool not exiting 2020-08-05 09:48:31 +00:00
Alex Ling 99a77966ad Add arm64v8 to Makefile and rename object files 2020-08-05 09:48:03 +00:00
Alex Ling d00b917575 Build the object file in Action 2020-08-04 17:24:36 +00:00
Alex Ling 4fd8334c37 Name the object file 2020-08-04 17:24:13 +00:00
Alex Ling 3aa4630558 Use Crystal 0.34.0 2020-08-04 17:23:19 +00:00
Alex Ling cde5af7066 Remove interactive prompt for easier use in docker 2020-08-04 12:57:40 +00:00
Alex Ling eb528e1726 Add the arm32v7 target to Makefile 2020-08-04 11:50:07 +00:00
Alex Ling 5e01cc38fe Fix downloaders 2020-08-04 11:36:36 +00:00
Alex Ling 9a787ccbc3 Formatting 2020-08-04 11:36:24 +00:00
Alex Ling 8a83c0df4e ARM support (#25, #78) 2020-08-04 11:00:33 +00:00
Alex Ling 87dea01917 Add ASCII banner, because we can :) 2020-08-02 17:52:52 +00:00
Alex Ling 586ee4f0ba Bump version to v0.10.0 2020-08-02 12:33:31 +00:00
Alex Ling 53f3387e1a Rephrase the plugin part in README 2020-08-02 12:32:14 +00:00
Alex Ling be5d1918aa Add offset to the sticky bar 2020-08-02 12:29:49 +00:00
Alex Ling df2cc0ffa9 Display nested titles and entries separately 2020-08-02 10:43:46 +00:00
Alex Ling b8cfc3a201 Remove unnecessary ids from HTML 2020-08-02 10:43:24 +00:00
Alex Ling 8dc60ac2ea Add select all button to the selection bar 2020-08-02 09:28:31 +00:00
Alex Ling 1719335d02 Add "Start Reading" section to home page (#92) 2020-08-01 15:17:18 +00:00
Alex Ling 0cd46abc66 Finish batch marking (#75) 2020-07-30 11:39:23 +00:00
Alex Ling e4fd7c58ee Add multi-select for cards in web interface 2020-07-30 08:32:00 +00:00
Alex Ling d4abee52db Fix .uk-card-media-top width 2020-07-30 08:29:41 +00:00
Alex Ling d29c94e898 Use Alpine.js 2020-07-30 08:28:54 +00:00
Alex Ling 1c19a91ee2 Merge branch 'master' of https://github.com/hkalexling/Mango 2020-07-29 12:19:24 +00:00
Alex Ling 7eb5c253e9 Bump version to v0.9.0 2020-07-29 10:07:36 +00:00
Alex Ling 22a660aabf Fix 500 for empty plugins 2020-07-29 10:07:10 +00:00
Alex Ling 6e9466c9d2 Rename plugin function search to listChapters 2020-07-29 07:15:55 +00:00
Alex Ling ab34fb260c Fix memory leak through archive.cr 2020-07-28 07:51:55 +00:00
Alex Ling 0e9a659828 Instantiate Plugin objects with IDs 2020-07-26 15:34:54 +00:00
Alex Ling 361d37d742 Decode plugin title before using it 2020-07-26 12:56:46 +00:00
Alex Ling c6adb4ee18 Fix plugin hot load 2020-07-26 12:56:29 +00:00
Alex Ling 8349fb68a4 Save last used plugin in local storage 2020-07-26 12:42:28 +00:00
Alex Ling 0e1e4de528 Add image placeholder to the reader page 2020-07-26 12:15:22 +00:00
Alex Ling b47788a85a Add download sub-nav to the mobile nav 2020-07-26 06:59:09 +00:00
Alex Ling f7004549b8 Remove MangaDex module tests 2020-07-26 06:48:49 +00:00
Alex Ling 8d99400c5f Return strings as header values 2020-07-25 16:19:39 +00:00
Alex Ling ce59acae7a Fix variable shadowing 2020-07-25 07:25:38 +00:00
Alex Ling 37c5911a23 Make plugin download table sortable 2020-07-25 07:20:22 +00:00
Alex Ling 8694b4beaf Show plugin info on the plugin download page 2020-07-24 15:02:05 +00:00
Alex Ling 3b315ad880 Pass status code and headers to plugin scripts 2020-07-24 13:56:54 +00:00
Alex Ling 33107670ce Use index.js instead of main.js 2020-07-24 09:30:10 +00:00
Alex Ling f116e2f1d0 Rename the state helper function to storage 2020-07-24 09:27:54 +00:00
Alex Ling ebf6221876 Rename Job#plugin_name to plugin_id 2020-07-24 07:50:50 +00:00
Alex Ling 2a910335af Easier to use mango.css helper method 2020-07-24 05:11:13 +00:00
Alex Ling 9ea26474b4 Fix formatting 2020-07-23 17:15:40 +00:00
Alex Ling df8a6ee6da Finish plugin functionalities 2020-07-23 17:15:40 +00:00
Alex Ling 70ea1711ce Handle selectable table dark mode more elegantly 2020-07-22 17:31:38 +00:00
Alex Ling 2773c1e67f Plugin download page WIP 2020-07-22 13:52:28 +00:00
Alex Ling dcfd1c8765 Expose @filename from the Plugin class 2020-07-22 13:51:45 +00:00
Alex Ling 10b6047df8 Process filenames before downloading 2020-07-22 13:51:03 +00:00
Alex Ling 8de735a2ca Add download dropdown in nav
and remove download manager from admin page
2020-07-22 12:12:29 +00:00
Alex Ling 6c2350c9c7 Fix modal and dropdown colors in dark mode
and get rid of the hacky `styleModal` function
2020-07-22 12:06:29 +00:00
Alex Ling a994c43857 Plugin downloader WIP 2020-07-22 09:09:02 +00:00
Alex Ling 7e4532fb14 Instantiate Plugins by plugin names 2020-07-22 09:09:02 +00:00
Alex Ling d184d6fba5 Expand path by home 2020-07-21 17:20:40 +00:00
Alex Ling 92f5a90629 Move pop to the Downloader classes 2020-07-21 17:20:03 +00:00
Alex Ling 2a36804e8d Validate returned JSON 2020-07-21 16:11:56 +00:00
Alex Ling 87b6e79952 Use macro to DRY the self.default method 2020-07-21 12:33:50 +00:00
Alex Ling b75a838e14 Move common code to Queue::Downloader 2020-07-21 12:32:48 +00:00
Alex Ling ae7c72ab85 Decouple Queue and related classes from MangaDex 2020-07-21 11:47:14 +00:00
Alex Ling 5cee68d76c Cleanup 2020-07-21 10:44:12 +00:00
Alex Ling f444496915 Check plugins dir exists before listing plugins 2020-07-21 10:08:30 +00:00
Alex Ling a812e3ed46 Add duktape.cr and the Plugin class 2020-07-21 09:30:45 +00:00
Alex Ling 1be089b53e Add open collective 2020-07-19 23:37:03 +08:00
Alex Ling a7f4e161de Add make setup 2020-07-19 13:53:50 +00:00
Alex Ling ba31eb0071 Use UIKit JS files from node_modules/ 2020-07-19 13:50:46 +00:00
Alex Ling 192474c950 Fix 404 icons 2020-07-19 13:29:05 +00:00
Alex Ling 87b72fbd30 Support 'System' theme setting (#91) 2020-07-19 10:58:23 +00:00
Alex Ling 6acfa02314 Remove unneeded property title_id from Entry 2020-07-18 13:34:55 +00:00
Alex Ling bdba7bdd13 Show unreadable archives in web interface (#49) 2020-07-18 13:29:03 +00:00
Alex Ling 1b244c68b8 Bump version to v0.8.0 2020-07-17 08:18:24 +00:00
Alex Ling 281f626e8c More tie-breaking 2020-07-16 13:17:58 +00:00
Alex Ling 5be4f51d7e Name partially downloaded cbz files .part (#90) 2020-07-16 13:16:36 +00:00
Alex Ling cd7782ba1e Respect custom sorting method in continue reading
(#86)
2020-07-15 17:06:54 +00:00
Alex Ling 6d97bc083c Break library.cr into multiple files 2020-07-15 16:12:36 +00:00
Alex Ling ff4b1be9ae Template cleanup 2020-07-15 16:04:03 +00:00
Alex Ling ba16c3db2f Add SortOptions.from_info_json 2020-07-15 15:33:26 +00:00
Alex Ling 69b06a8352 Use auto sort to break ties when sorting 2020-07-15 15:13:38 +00:00
Alex Ling 687788767f Use auto when an unknown sorting method is passed 2020-07-15 10:47:27 +00:00
Alex Ling 94a1e63963 Handle library/title sorting on backend (#86) 2020-07-15 10:34:53 +00:00
Alex Ling 360913ee78 Add chapter sort 2020-07-12 08:59:40 +00:00
Alex Ling ea366f263a Move require "big" to relevant util file 2020-07-12 08:53:46 +00:00
Alex Ling 0d11cb59e9 Break util.cr into multiple files 2020-07-12 08:53:04 +00:00
Alex Ling 2208f90d8e Properly close archive files after validating them 2020-07-11 15:51:57 +00:00
Alex Ling 07100121ef Bump version to v0.7.3 2020-07-05 14:36:12 +00:00
Alex Ling a0e550569e Use archive.cr v0.3.0 for 32bit support 2020-07-05 14:34:19 +00:00
Alex Ling bbbe2e0588 Move uikit.less 2020-07-04 11:17:27 +00:00
Alex Ling 9d31b24e8c Fix nested a tags 2020-07-04 11:10:32 +00:00
Alex Ling 38ba324fa9 Save the sorting option in local storage (#76) 2020-07-04 09:47:51 +00:00
Alex Ling c00016fa19 Remove link to title page from the entry modal 2020-07-04 08:56:58 +00:00
Alex Ling 4d5a305d1b Reduce card font size and link to the title pages
(#84)
2020-07-04 08:56:58 +00:00
Alex Ling f9ca52ee2f Keep progress label color in dark mode (#85) 2020-07-03 06:53:39 +00:00
Alex Ling f6c393545c Only show started entries in "continue reading"
(#83)
2020-07-03 06:52:50 +00:00
Alex Ling 466aee62fe Bump version to v0.7.2 2020-07-01 14:15:29 +00:00
Alex Ling eab0800376 Improve scan performance (#79) 2020-07-01 14:01:26 +00:00
Alex Ling 1725f42698 Use HTML.escape to escape XML 2020-07-01 13:27:30 +00:00
Alex Ling f5cdf8b7b6 Explicitly register supported mime types (#82) 2020-07-01 13:21:14 +00:00
Alex Ling fe082e7537 Escape illegal characters in XML (#82) 2020-06-30 16:46:47 +00:00
Alex Ling c87b96dd0b Improve performance for library and title pages 2020-06-24 16:29:34 +00:00
Alex Ling 9d76ca8c24 Improve home page loading time (#81) 2020-06-24 15:52:26 +00:00
Alex Ling 5f21653e07 Bump version to v0.7.1 2020-06-20 16:25:14 +00:00
Alex Ling 0035cd9177 Revert "Upgrade Crystal to 0.35.1"
Kemal is having some issues in 0.35.0: https://github.com/kemalcr/kemal/issues/575
2020-06-20 16:24:01 +00:00
Alex Ling 899b221842 Merge branch 'dev' 2020-06-20 13:50:37 +00:00
Alex Ling a317086f81 Bump version to v0.7.0 2020-06-20 13:39:14 +00:00
Alex Ling b83313b231 Set recently added group range to 1 day 2020-06-20 13:12:13 +00:00
Alex Ling 62af879bfa Upgrade Crystal to 0.35.1 2020-06-20 13:12:13 +00:00
Alex Ling 96f98f6c78 Rename and format ECR files 2020-06-19 11:34:03 +00:00
Alex Ling 841d5051cb Copy robots.txt to dist/ in gulpfile 2020-06-18 15:09:18 +00:00
Alex Ling 0768e2177b Bring back original behavior for recently added
(#37 https://github.com/hkalexling/Mango/issues/37#issuecomment-644748066)
2020-06-17 16:17:29 +00:00
Alex Ling 0e4d67cf29 Hide the progress badge with incorrect value 2020-06-17 16:16:35 +00:00
Alex Ling 00fcc881ee Start from page 1 if the entry has been completed
(#71)
2020-06-16 06:17:52 +00:00
Alex Ling ca8d9efcfd Show entry display name and path in reader modal
(#71)
2020-06-16 06:06:32 +00:00
Alex Ling 0e7be6392d Fix incorrect modal colors on the reader page 2020-06-16 06:06:32 +00:00
Alex Ling 4af5258602 Show Mango version on the admin page (#71) 2020-06-16 05:26:34 +00:00
Alex Ling 23c6256552 Merge branch 'feature/color-label' into dev 2020-06-16 05:16:27 +00:00
Alex Ling ef0e3fd346 Add color to labels in dark mode (#70) 2020-06-16 05:15:39 +00:00
Alex Ling b70fad13a7 Restrict "recently added" from 3 months to 1 2020-06-15 14:54:28 +00:00
Alex Ling d2f9735250 Add space between entry title and button in modal 2020-06-15 14:53:16 +00:00
Alex Ling 06d6311080 Display book percentage in "recently added" 2020-06-15 14:32:37 +00:00
Alex Ling 674da55bde Add entry download button (#45) 2020-06-15 12:54:42 +00:00
Alex Ling dc084aff7c Support webp (#69, nice) 2020-06-15 12:35:44 +00:00
Alex Ling 4c2cf64f53 Limit load_progress to @pages 2020-06-15 12:34:51 +00:00
Alex Ling f4c4bb536c Include nested entries in continue reading 2020-06-15 12:10:31 +00:00
Alex Ling 47edb3008b Refactor get_recently_added_entries 2020-06-15 12:09:17 +00:00
Alex Ling e28dadc94e Add started? and deep_titles helper methods 2020-06-15 12:05:59 +00:00
Alex Ling 3dc9bd2264 Add finished? helper method 2020-06-15 09:48:41 +00:00
Alex Ling 9302601307 Move relevant methods from Title to Entry 2020-06-13 15:54:55 +00:00
Alex Ling 650ba98039 Merge pull request #67 from flying-sausages/patch-1
Added Use-case to Feature Request template
2020-06-11 22:45:02 +08:00
flying-sausages bb2173788b Added Use-case to Feature Request template 2020-06-11 14:14:41 +01:00
Alex Ling c8be2849b9 Show progress for titles and nested titles 2020-06-11 12:36:25 +00:00
Alex Ling aa269f26ee Fix incorrect breadcrumb menu order 2020-06-11 12:34:13 +00:00
Alex Ling 5c26b0d6dc Handle nested titles in the recently added section 2020-06-11 10:03:34 +00:00
Alex Ling c9d3c35bdd Add robots.txt 2020-06-09 15:48:08 +00:00
Alex Ling 9255de710f Link to Wiki in README 2020-06-09 15:12:23 +00:00
Alex Ling 39b251774f Bump version to v0.6.1 [skip ci] 2020-06-09 15:08:15 +00:00
Alex Ling 156e511d4a Fix failed build (omitted parentheses) 2020-06-09 14:54:23 +00:00
Alex Ling 5cd6f3eacb Fix incorrect login redirect (#64) 2020-06-09 14:46:45 +00:00
Alex Ling a0e5a03052 DRY html modal and head 2020-06-09 10:34:24 +00:00
Alex Ling e53641add1 Handle the rare case when renamed string is ".." 2020-06-09 09:42:28 +00:00
Alex Ling 45cdfd5306 Merge branch 'fix/mangadex-slash' into dev 2020-06-09 09:31:17 +00:00
Alex Ling 3d352ed062 Add test for slash escaping 2020-06-09 09:28:37 +00:00
Alex Ling bac7be5163 Escape slash in filename when downloading (#62) 2020-06-09 09:25:20 +00:00
Alex Ling 717d44e029 Refactor get_recently_added_entries method 2020-06-09 05:37:10 +00:00
Alex Ling 8da4475a74 Remove duplicate title ID (#56) 2020-06-08 15:55:40 +00:00
Alex Ling 680504779f Use component template on home page 2020-06-08 15:51:42 +00:00
Alex Ling 926d0e66a5 Formatting 2020-06-08 15:29:05 +00:00
Alex Ling 0f3dd51d6b Respect base URL 2020-06-08 15:24:35 +00:00
Alex Ling 53c3798691 Merge branch 'feature/home' into dev 2020-06-08 15:11:09 +00:00
Jared Turner 6d4e8ea544 Show config path for empty libraries and link to Admin for manual re-scan 2020-06-08 15:24:51 +01:00
Jared Turner 0bd94a2290 Add config path to Config 2020-06-08 15:24:17 +01:00
Jared Turner cff599f688 refactor get_recently_added_entries, new_user and empty_library 2020-06-08 15:23:36 +01:00
Jared Turner fa85d9834f Onboarding for new libraries and new users 2020-06-07 18:40:31 +01:00
Jared Turner aaf0a3c6af Group Recently Added by neighbouring Title 2020-06-07 18:39:05 +01:00
Jared Turner 5ed2a8affa Add Library link to mobile nav 2020-06-07 18:36:51 +01:00
Alex Ling de690fbf29 Store token and callback URI in memory session 2020-06-07 16:18:34 +00:00
Alex Ling 12c3c3f356 Bump version to v0.6.0 2020-06-06 15:45:44 +00:00
Alex Ling 1ddcabcc12 Use component templates 2020-06-06 12:00:02 +00:00
Alex Ling 8b04f2c96b Remove comment in the OPDS xml file [skip ci] 2020-06-05 16:41:55 +00:00
Alex Ling 66e2fc138a Mention OPDS support in README [skip ci] 2020-06-05 16:15:55 +00:00
Alex Ling 6817113523 Clean up 2020-06-05 15:25:41 +00:00
Alex Ling 6ad4385b18 Respect base URL in OPDS feed 2020-06-05 15:18:46 +00:00
Alex Ling 012fd71ab4 Use a helper function to set token cookie 2020-06-05 14:31:12 +00:00
Alex Ling 373ff6520a Merge branch 'feature/opds' into dev 2020-06-05 14:28:36 +00:00
Alex Ling 8a0e9250c8 Finish OPDS 2020-06-05 14:21:47 +00:00
Alex Ling 871a5fe755 Add render_xml helper function 2020-06-05 14:21:47 +00:00
Alex Ling 1493c3de90 Set token cookie after successful basic auth 2020-06-05 14:21:47 +00:00
Jared Turner 808074e478 Add Recently Added to home 2020-06-05 15:13:19 +01:00
Jared Turner 49193b9b00 Merge branch 'feature/home' of github.com:hkalexling/Mango into feature/home 2020-06-04 19:44:07 +01:00
jaredlt 1cb470fb2d Merge pull request #57 from hkalexling/feature/home-ctime
Add `ctime` helper function
2020-06-04 19:43:46 +01:00
Alex Ling e443176a79 Add ctime helper function 2020-06-04 16:31:49 +00:00
Alex Ling bec257c99f Update HTML description meta tag 2020-06-04 15:07:32 +00:00
Alex Ling f2df493d79 Add Ko-Fi link [skip ci] 2020-06-04 14:54:46 +00:00
Alex Ling b74f61c025 Bump version to v0.5.2 [skip ci] 2020-06-04 14:52:38 +00:00
Alex Ling c76c287e66 Fix URL of uploaded images when using base URL 2020-06-04 12:38:38 +00:00
Alex Ling 8e7eaa680a Fix favicon for base URL (#55) [skip-ci] 2020-06-04 05:43:37 +00:00
Alex Ling 30cdb3ec8f Remove duplicate title ID (#56) 2020-06-04 05:37:20 +00:00
Alex Ling 9c367e7d35 Format HTML files with html-beautify 2020-06-04 05:36:39 +00:00
Jared Turner 4f5e05c008 refactor continue reading into Library class 2020-06-03 13:48:49 +01:00
Alex Ling d2f95e5970 Bump version to v0.5.1 2020-06-03 08:22:05 +00:00
Alex Ling 82bcd03f15 Always create initial user if the DB is empty when started 2020-06-03 08:20:40 +00:00
Alex Ling fe799f30c8 Make the user listing command handles empty DB 2020-06-03 08:19:40 +00:00
Alex Ling 54123917af Empty ARGV before starting Kemal (#53) 2020-06-03 07:55:18 +00:00
Alex Ling 3b737c0bee Add library URL in README [skip ci] 2020-06-02 15:57:14 +00:00
Alex Ling 14bf4da06c Merge branch 'dev' 2020-06-02 15:45:11 +00:00
Alex Ling a72dfcecd3 Bump version to v0.5.0 2020-06-02 15:29:32 +00:00
Alex Ling 160a249dc6 Update CLI help message in README 2020-06-02 15:26:38 +00:00
Alex Ling f9a2534f80 Mention CBR support in README 2020-06-02 15:21:08 +00:00
Alex Ling 06fe2ccf16 Handle escaped characters when filtering (#51) [skip ci] 2020-06-02 15:08:43 +00:00
Jared Turner 13c0878357 Merge branch 'feature/home' of github.com:hkalexling/Mango into feature/home 2020-06-01 15:29:36 +01:00
Jared Turner 3ef6a7bfc4 continue reading sorted by last read 2020-06-01 15:29:18 +01:00
Alex Ling e214e00dfb Include port number in token 2020-06-01 13:50:51 +00:00
Alex Ling 9b5aea223d Promote archive error log level to warning 2020-06-01 13:38:15 +00:00
Alex Ling 60100c51fe Add send_attachment function for direct download 2020-06-01 13:21:10 +00:00
Alex Ling 27c111d273 Handle basic auth for OPDS 2020-06-01 13:20:05 +00:00
Alex Ling 1b9d83f367 Report if archive is not readable #49 2020-06-01 04:54:28 +00:00
Alex Ling 96b8186add Merge branch 'feature/admin-cli' into dev 2020-06-01 04:33:27 +00:00
Alex Ling 27dab3c989 Disable initial user creation in spec 2020-05-31 15:26:11 +00:00
Alex Ling bcb95d1462 Make validate_archive more thorough 2020-05-31 15:14:17 +00:00
Alex Ling 4371c7877d Use base URL in cookies path 2020-05-31 14:34:42 +00:00
Alex Ling d72d635c68 Add admin/user sub-command 2020-05-31 14:30:45 +00:00
Alex Ling b724b4d508 Move username/password validation to Storage class 2020-05-31 14:26:20 +00:00
Alex Ling 8bbbe650f1 Allow skipping initial user creation 2020-05-31 14:25:15 +00:00
Alex Ling 651bd17612 Rewrite option parsing using clim and add the admin subcommand 2020-05-30 15:14:39 +00:00
Alex Ling dd01e632a2 Promote ameba from development dependency to regular dependency
So I can use it in CI while keeping the `--production` flag in Makefile
2020-05-29 16:33:15 +00:00
Alex Ling 43ee8f3b85 Pass in production flag when installing shards 2020-05-29 16:23:48 +00:00
Alex Ling 4841f90cc1 Remove edit buttons from home 2020-05-29 15:51:01 +00:00
Alex Ling bedcac4e35 Add missing libarchive-dev library 2020-05-29 14:28:18 +00:00
Alex Ling 5260a82e88 Add libarchive libraries to Docker and build files 2020-05-29 14:04:17 +00:00
Alex Ling 1efb300988 Use archive.cr v0.1.0 2020-05-29 14:00:59 +00:00
Alex Ling 6b43ee7fe5 Add RAR/CBR support 2020-05-29 13:45:25 +00:00
Jared Turner e99d7b8b29 Merge branch 'feature/home' of github.com:hkalexling/Mango into feature/home 2020-05-29 13:31:00 +01:00
Jared Turner d2ad7fef77 WIP last_read property for Entries 2020-05-29 13:26:47 +01:00
Jared Turner ddb6a860ae add 'jump to title' to home modal 2020-05-24 10:35:35 +01:00
Alex Ling 3039031924 Merge branch 'master' of https://github.com/hkalexling/Mango 2020-05-24 06:47:19 +00:00
Alex Ling 8665616c2e Bump version to v0.4.0 2020-05-24 06:36:40 +00:00
Alex Ling 4453b0ee9f Link to development guideline in README [skip ci] 2020-05-24 14:34:19 +08:00
Alex Ling 487154e68c Add base url and rename rules to README [skip ci] 2020-05-24 14:33:05 +08:00
Alex Ling 60609263ab Explicitly set icon size (#40) [skip ci] 2020-05-23 14:47:35 +00:00
Alex Ling 4a245d2504 Check supplied base url has leading slash and append tailing slash if needed 2020-05-23 14:30:41 +00:00
Alex Ling 48c3a82078 Use base url when generating cover URLs 2020-05-23 14:16:56 +00:00
Alex Ling 4a59459773 Use base url in JS files 2020-05-23 14:18:14 +00:00
Alex Ling eefa8c3982 Use base url in some hardcoded URLs 2020-05-23 14:17:11 +00:00
Alex Ling 8fe2f3b4cc Use base url in views 2020-05-23 14:16:56 +00:00
Alex Ling 6a9105605d Fix library link in the breadcrumb menu 2020-05-23 12:16:08 +00:00
Alex Ling 60d4cee0a9 Respect base url setting when redirecting 2020-05-23 10:42:59 +00:00
Alex Ling 8658cb8306 Add base url to config 2020-05-23 10:42:39 +00:00
Alex Ling c74a01f546 Remove unnecessary JS files from home.ecr 2020-05-21 09:10:46 +00:00
Alex Ling 2aeb38a271 Remove edit button from home screen 2020-05-21 09:06:50 +00:00
Jared Turner a2c7638141 refactor on deck to continue reading and show percentages on home 2020-05-20 10:38:23 +01:00
Alex Ling c35e840694 Refactor the / route 2020-05-19 12:16:32 +00:00
Alex Ling ff6e64f12a Refactor get_on_deck_entry 2020-05-19 12:05:15 +00:00
Jared Turner 16fa27e4f6 update comments 2020-05-18 21:09:11 +01:00
Jared Turner 16734c2c59 rename root to library and add home with on deck WIP 2020-05-18 21:06:14 +01:00
Jared Turner 392b3d8339 fix load_percetage method name typo 2020-05-18 20:32:09 +01:00
Alex Ling d4e523c337 [skip ci] allow skip CI 2020-05-17 14:08:46 +00:00
Alex Ling d49c0092c2 Generate artifact 2020-05-17 13:57:28 +00:00
Alex Ling d75009f088 Rename scripts/ to dev/ 2020-05-17 13:44:10 +00:00
Alex Ling d416dc6618 Use rename when downloading 2020-05-17 06:29:13 +00:00
Alex Ling 7233e6e5c3 Type annotate the self.default methods 2020-05-17 06:28:33 +00:00
Alex Ling bd8ae9497f Initialize the downloader when started 2020-05-07 15:42:31 +00:00
Alex Ling 34b11dc2c7 Only hijack HTTP 500 when in release mode 2020-05-07 15:41:02 +00:00
Alex Ling 30dea57346 Use singleton in tests 2020-05-07 10:12:58 +00:00
Alex Ling 7448592216 Optionally pass in db path for testing 2020-05-07 10:12:58 +00:00
Alex Ling 049bd3ab2c Fix long lines 2020-05-07 10:12:58 +00:00
Alex Ling c3608c101b Enforce 80 characters limit in make check 2020-05-07 10:12:58 +00:00
Alex Ling 1bec9f0108 Use singleton in various classes 2020-05-07 10:12:58 +00:00
Alex Ling 09b297cd8e Add rename method to Manga and Chapter 2020-05-07 10:12:06 +00:00
Alex Ling b7cd55e692 Add rename rules to config 2020-05-07 10:11:45 +00:00
Alex Ling 986939ecb6 Add tests for the Rename module 2020-05-07 10:01:32 +00:00
Alex Ling a5e97af3a3 Use abstract class in the Rename module 2020-05-03 16:31:00 +00:00
Alex Ling 4cee5faecd Allow | character outside of patterns 2020-05-03 16:30:35 +00:00
Alex Ling 711add74ef Allow spaces in patterns 2020-05-03 16:29:54 +00:00
Alex Ling f6f09c54bc Add Rename module 2020-05-03 12:02:12 +00:00
Alex Ling 0f58ebb87b Ignore markdown files 2020-05-03 12:01:57 +00:00
Alex Ling 46347a8fe4 Update README.md 2020-04-23 14:49:06 +08:00
Alex Ling a354d811d9 Merge branch 'dev' 2020-04-22 14:36:36 +00:00
Alex Ling 22d757362a Update README.md 2020-04-22 22:34:02 +08:00
Alex Ling 8afcea7e87 Update README.md 2020-04-22 22:31:40 +08:00
Alex Ling fb05e913a0 Limit cover image types to png/jpeg in the web UI 2020-04-20 07:36:55 +00:00
Alex Ling 490888ad71 Bump version to 0.3.0 2020-04-19 16:23:00 +00:00
Alex Ling 20d71bfa81 Finish #30 2020-04-19 16:11:23 +00:00
Alex Ling ec6a7bd3d9 Read/unread a directory with API 2020-04-19 15:47:36 +00:00
Alex Ling b449d906ec Merge branch 'cover' into dev 2020-04-19 14:39:19 +00:00
Alex Ling f66bec5545 Update frontend for cover upload 2020-04-19 14:33:24 +00:00
Alex Ling ce5f444012 Remove debug code in upload handler 2020-04-19 14:32:58 +00:00
Alex Ling 8506044232 Handle errors in the "/" endpoint 2020-04-14 06:08:10 +00:00
Alex Ling 079dd8e280 Fix layout macro message displaying bug 2020-04-14 06:08:10 +00:00
Alex Ling 8262a163db Finish the API endpoint for cover upload 2020-04-14 06:09:23 +00:00
Alex Ling d6b22ef736 Don't return from DB blocks 2020-04-10 15:24:49 +00:00
Alex Ling 39f4897fc5 Set status as "Error" if downloaded zip is invalid
(#29)
2020-04-08 10:31:30 +00:00
Alex Ling fc6a33e5fd Update Makefile 2020-04-08 07:18:25 +00:00
Alex Ling 7d97d21d40 Run Ameba and Crystal formatting tool on push 2020-04-08 07:09:54 +00:00
Alex Ling fcf9d39047 Project-wise refactoring to follow Ameba 2020-04-08 06:45:45 +00:00
Alex Ling d33cae7618 Use Ameba 2020-04-08 06:45:45 +00:00
Alex Ling 8b184ed48d Project-wise code formatting 2020-04-08 05:25:12 +00:00
Alex Ling d3309a810b Update bug_report.md 2020-04-07 22:15:07 +08:00
Alex Ling 3866c81588 Use the updated Logger class in spec 2020-04-07 13:26:09 +00:00
Alex Ling 2c31f594a4 Use the new Log module in Crystal 0.34.0 2020-04-07 12:58:42 +00:00
Alex Ling c572c56a39 Upgrade Crystal version to 0.34.0 2020-04-07 12:57:50 +00:00
Alex Ling e670a083a3 Update shards.lock 2020-04-07 12:57:50 +00:00
Alex Ling 9b23e1759d Update shards.lock 2020-04-07 12:57:50 +00:00
Alex Ling 14e3470b12 Hide rename buttons when the login user is not admin 2020-04-07 12:57:50 +00:00
Alex Ling 8ce51a6163 Hide the "Admin" and "Download" buttons when user is not admin 2020-04-07 12:57:50 +00:00
Alex Ling 1d4237d687 Pass in admin information when rendering all pages 2020-04-07 12:57:50 +00:00
Alex Ling b7c0515af7 Fix dark mode on login page 2020-04-07 12:57:50 +00:00
Alex Ling 75edfcdb5b Set and load display names in frontend 2020-04-07 12:57:50 +00:00
Alex Ling 51d19328be Set up API endpoint for setting display names 2020-04-07 12:57:50 +00:00
Alex Ling d405498af4 Update shards.lock 2020-04-07 04:01:04 +00:00
Alex Ling 696f79aea1 Merge pull request #28 from noirscape/env-file
Use a .env file for docker-compose configuration.
2020-04-07 11:43:32 +08:00
noirscape d2da8d0b9a docker: Use a .env file 2020-04-06 21:49:14 +02:00
Alex Ling 4e961192d4 Update README.md 2020-04-06 22:44:45 +08:00
Alex Ling 8b90524a2c Create dockerhub.yml 2020-04-06 21:45:08 +08:00
Alex Ling c9b8770b9f Bump version to v0.2.5 2020-04-02 09:12:35 +00:00
Alex Ling e568ec8878 Fix the unexpected sorting behavior on Chrome 2020-04-02 09:06:16 +00:00
Alex Ling ac3df03d88 Show page counts on chapter cards 2020-04-02 05:44:29 +00:00
Alex Ling 7c9728683c On the title page, hide progress label of nested titles 2020-04-02 00:16:19 +00:00
Alex Ling d921d04abf Bump version to v0.2.4 2020-04-01 23:32:16 +00:00
Alex Ling 5400c8c8ef Fix a UI bug that shows "resume download" button on download manager even when the downloading process is not paused 2020-04-01 23:21:32 +00:00
Alex Ling 58e96cd4fe Watch the title element size for change 2020-04-01 06:13:03 +00:00
Alex Ling aa09f3a86f Only show tooltips for truncated titles 2020-04-01 05:59:46 +00:00
Alex Ling a5daded453 Fix the width and height of cover images (#23) 2020-04-01 04:51:57 +00:00
Alex Ling 4968cb8e18 Add tooltips to show un-truncated titles 2020-04-01 04:49:53 +00:00
Alex Ling 27c6e02da8 Run the truncate function after DOM is ready 2020-04-01 04:48:53 +00:00
Alex Ling 68d1b55aea Limit title text height in CSS 2020-04-01 04:47:55 +00:00
Alex Ling 32dc3e84b9 Lazy load images in library/title page to improve page load time 2020-03-31 08:44:07 +00:00
Alex Ling 460fcdf2f5 Limit the number of lines to display in card titles 2020-03-30 20:36:27 +00:00
Alex Ling c6369f9f26 Prevent flash of white in cards 2020-03-30 20:35:30 +00:00
127 changed files with 8314 additions and 3182 deletions
+102
View File
@@ -0,0 +1,102 @@
{
"projectName": "Mango",
"projectOwner": "hkalexling",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "none",
"contributors": [
{
"login": "hkalexling",
"name": "Alex Ling",
"avatar_url": "https://avatars1.githubusercontent.com/u/7845831?v=4",
"profile": "https://github.com/hkalexling/",
"contributions": [
"code",
"doc",
"infra"
]
},
{
"login": "jaredlt",
"name": "jaredlt",
"avatar_url": "https://avatars1.githubusercontent.com/u/8590311?v=4",
"profile": "https://github.com/jaredlt",
"contributions": [
"code",
"ideas",
"design"
]
},
{
"login": "shincurry",
"name": "ココロ",
"avatar_url": "https://avatars1.githubusercontent.com/u/4946624?v=4",
"profile": "https://windisco.com/",
"contributions": [
"infra"
]
},
{
"login": "noirscape",
"name": "Valentijn",
"avatar_url": "https://avatars0.githubusercontent.com/u/13433513?v=4",
"profile": "https://catgirlsin.space/",
"contributions": [
"infra"
]
},
{
"login": "flying-sausages",
"name": "flying-sausages",
"avatar_url": "https://avatars1.githubusercontent.com/u/23618693?v=4",
"profile": "https://github.com/flying-sausages",
"contributions": [
"doc",
"ideas"
]
},
{
"login": "XavierSchiller",
"name": "Xavier",
"avatar_url": "https://avatars1.githubusercontent.com/u/22575255?v=4",
"profile": "https://github.com/XavierSchiller",
"contributions": [
"infra"
]
},
{
"login": "WROIATE",
"name": "Jarao",
"avatar_url": "https://avatars3.githubusercontent.com/u/44677306?v=4",
"profile": "https://github.com/WROIATE",
"contributions": [
"infra"
]
},
{
"login": "Leeingnyo",
"name": "이인용",
"avatar_url": "https://avatars0.githubusercontent.com/u/6760150?v=4",
"profile": "https://github.com/Leeingnyo",
"contributions": [
"code"
]
},
{
"login": "h45h74x",
"name": "Simon",
"avatar_url": "https://avatars1.githubusercontent.com/u/27204033?v=4",
"profile": "http://h45h74x.eu.org",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}
+9
View File
@@ -0,0 +1,9 @@
Lint/UselessAssign:
Excluded:
- src/routes/*
- src/server.cr
Lint/UnusedArgument:
Excluded:
- src/routes/*
Metrics/CyclomaticComplexity:
Enabled: false
+2
View File
@@ -0,0 +1,2 @@
node_modules
lib
+2
View File
@@ -1,3 +1,5 @@
# These are supported funding model platforms # These are supported funding model platforms
open_collective: mango
patreon: hkalexling patreon: hkalexling
ko_fi: hkalexling
+1 -1
View File
@@ -26,7 +26,7 @@ A clear and concise description of what you expected to happen.
- Mango Version [e.g. v0.1.0] - Mango Version [e.g. v0.1.0]
**Docker (if you are running Mango in a Docker container)** **Docker (if you are running Mango in a Docker container)**
- The `docker-compose.yml` file you are using - The `docker-compose.yml` file you are using, or your `.env` file.
**Additional context** **Additional context**
Add any other context about the problem here. Add screenshots if applicable. Add any other context about the problem here. Add screenshots if applicable.
+4 -1
View File
@@ -8,10 +8,13 @@ assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
**Describe the solution you'd like** **Describe the solution you'd like**
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
**Describe a small use-case for this feature request**
How would you imagine this to be used? What would be the advantage of this for the users of the application?
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.
+21 -5
View File
@@ -2,23 +2,39 @@ name: Build
on: on:
push: push:
branches: [ master, dev ] branches: [ master, dev, hotfix/* ]
pull_request: pull_request:
branches: [ master, dev ] branches: [ master, dev ]
jobs: jobs:
build: build:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: crystallang/crystal:0.32.1-alpine image: crystallang/crystal:0.35.1-alpine
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: apk add --no-cache yarn yaml sqlite-static run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
- name: Build - name: Build
run: make run: make static || make static
- name: Linter
run: make check
- name: Run tests - name: Run tests
run: make test run: make test
- name: Upload binary
uses: actions/upload-artifact@v2
with:
name: mango
path: mango
- name: build arm32v7 object file
run: make arm32v7 || make arm32v7
- name: build arm64v8 object file
run: make arm64v8 || make arm64v8
- name: Upload object files
uses: actions/upload-artifact@v2
with:
name: object files
path: ./*.o
+19
View File
@@ -0,0 +1,19 @@
name: Publish Dockerhub
on:
release:
types: [published]
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get release version
id: get_version
run: echo "RELEASE_VERSION=$(echo ${GITHUB_REF:10})" >> $GITHUB_ENV
- name: Publish to Dockerhub
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: hkalexling/mango
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tags: "latest,${{ env.RELEASE_VERSION }}"
+6 -1
View File
@@ -7,4 +7,9 @@ node_modules
yarn.lock yarn.lock
dist dist
mango mango
docker-compose.yml .env
*.md
public/css/uikit.css
public/img/*.svg
public/js/*.min.js
public/css/*.css
+3 -4
View File
@@ -1,11 +1,10 @@
FROM crystallang/crystal:0.32.1-alpine AS builder FROM crystallang/crystal:0.35.1-alpine AS builder
WORKDIR /Mango WORKDIR /Mango
COPY . . COPY . .
COPY package*.json . RUN apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
RUN apk add --no-cache yarn yaml sqlite-static \ RUN make static || make static
&& make static
FROM library/alpine FROM library/alpine
+14
View File
@@ -0,0 +1,14 @@
FROM arm32v7/ubuntu:18.04
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.35.1 && make deps && cd ..
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
COPY mango-arm32v7.o .
RUN cc 'mango-arm32v7.o' -o 'mango' -rdynamic -lxml2 -L/image_size.cr/ext/libwebp -lwebp -L/image_size.cr/ext/stbi -lstbi /myhtml/src/ext/modest-c/lib/libmodest_static.a -L/duktape.cr/src/.build/lib -L/duktape.cr/src/.build/include -lduktape -lm `pkg-config libarchive --libs` -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lgmp -lsqlite3 -lyaml -lpcre -lm /usr/lib/arm-linux-gnueabihf/libgc.so -lpthread /crystal/src/ext/libcrystal.a -levent -lrt -ldl -L/usr/bin/../lib/crystal/lib -L/usr/bin/../lib/crystal/lib
CMD ["./mango"]
+14
View File
@@ -0,0 +1,14 @@
FROM arm64v8/ubuntu:18.04
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.35.1 && make deps && cd ..
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
COPY mango-arm64v8.o .
RUN cc 'mango-arm64v8.o' -o 'mango' -rdynamic -lxml2 -L/image_size.cr/ext/libwebp -lwebp -L/image_size.cr/ext/stbi -lstbi /myhtml/src/ext/modest-c/lib/libmodest_static.a -L/duktape.cr/src/.build/lib -L/duktape.cr/src/.build/include -lduktape -lm `pkg-config libarchive --libs` -lz `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'` `command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'` -lgmp -lsqlite3 -lyaml -lpcre -lm /usr/lib/aarch64-linux-gnu/libgc.so -lpthread /crystal/src/ext/libcrystal.a -levent -lrt -ldl -L/usr/bin/../lib/crystal/lib -L/usr/bin/../lib/crystal/lib
CMD ["./mango"]
+19 -4
View File
@@ -1,4 +1,4 @@
PREFIX=/usr/local PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin INSTALL_DIR=$(PREFIX)/bin
all: uglify | build all: uglify | build
@@ -7,14 +7,18 @@ uglify:
yarn yarn
yarn uglify yarn uglify
setup: libs
yarn
yarn gulp dev
build: libs build: libs
crystal build src/mango.cr --release --progress crystal build src/mango.cr --release --progress --error-trace
static: uglify | libs static: uglify | libs
crystal build src/mango.cr --release --progress --static crystal build src/mango.cr --release --progress --static --error-trace
libs: libs:
shards install shards install --production
run: run:
crystal run src/mango.cr --error-trace crystal run src/mango.cr --error-trace
@@ -22,6 +26,17 @@ run:
test: test:
crystal spec crystal spec
check:
crystal tool format --check
./bin/ameba
./dev/linewidth.sh
arm32v7:
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='arm-linux-gnueabihf' -o mango-arm32v7
arm64v8:
crystal build src/mango.cr --release --progress --error-trace --cross-compile --target='aarch64-linux-gnu' -o mango-arm64v8
install: install:
cp mango $(INSTALL_DIR)/mango cp mango $(INSTALL_DIR)/mango
+73 -16
View File
@@ -1,6 +1,3 @@
![banner](./public/img/banner-paddings.png) ![banner](./public/img/banner-paddings.png)
# Mango # Mango
@@ -10,32 +7,41 @@
Mango is a self-hosted manga server and reader. Its features include Mango is a self-hosted manga server and reader. Its features include
- Multi-user support - Multi-user support
- OPDS support
- Dark/light mode switch - Dark/light mode switch
- Supports both `.zip` and `.cbz` formats - Supported formats: `.cbz`, `.zip`, `.cbr` and `.rar`
- Supports nested folders in library - Supports nested folders in library
- Automatically stores reading progress - Automatically stores reading progress
- Thumbnail generation
- Built-in [MangaDex](https://mangadex.org/) downloader - Built-in [MangaDex](https://mangadex.org/) downloader
- Supports [plugins](https://github.com/hkalexling/mango-plugins) to download from thrid-party sites
- The web reader is responsive and works well on mobile, so there is no need for a mobile app - The web reader is responsive and works well on mobile, so there is no need for a mobile app
- All the static files are embedded in the binary, so the deployment process is easy and painless - All the static files are embedded in the binary, so the deployment process is easy and painless
Please check the [Wiki](https://github.com/hkalexling/Mango/wiki) for more information.
## Installation ## Installation
### Pre-built Binary ### Pre-built Binary
1. Simply download the pre-built binary file `mango` for the latest [release](https://github.com/hkalexling/Mango/releases). All the dependencies are statically linked, and it should work with most Linux systems on amd64. Simply download the pre-built binary file `mango` for the latest [release](https://github.com/hkalexling/Mango/releases). All the dependencies are statically linked, and it should work with most Linux systems on amd64.
### Docker ### Docker
1. Make sure you have docker installed and running. You will also need `docker-compose` 1. Make sure you have docker installed and running. You will also need `docker-compose`
2. Clone the repository 2. Clone the repository
3. Copy `docker-compose.example.yml` to `docker-compose.yml` 3. Copy the `env.example` file to `.env`
4. Modify the `volumes` in `docker-compose.yml` to point the directories to desired locations on the host machine 4. Fill out the values in the `.env` file. Note that the main and config directories will be created if they don't already exist. The files in these folders will be owned by the root user
5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside 5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside
6. Head over to `localhost:9000` to log in 6. Head over to `localhost:9000` (or a different port if you changed it) to log in
### Docker (via Dockerhub)
The official docker images are available on [Dockerhub](https://hub.docker.com/r/hkalexling/mango).
### Build from source ### Build from source
1. Make sure you have Crystal, Node and Yarn installed. You might also need to install the development headers for `libsqlite3` and `libyaml`. 1. Make sure you have `crystal`, `shards` and `yarn` installed. You might also need to install the development headers of some libraries. Please see the [Dockerfile](https://github.com/hkalexling/Mango/blob/master/Dockerfile) for the full list of dependencies
2. Clone the repository 2. Clone the repository
3. `make && sudo make install` 3. `make && sudo make install`
4. Start Mango by running the command `mango` 4. Start Mango by running the command `mango`
@@ -46,11 +52,21 @@ Mango is a self-hosted manga server and reader. Its features include
### CLI ### CLI
``` ```
Mango e-manga server/reader. Version 0.2.0 Mango - Manga Server and Web Reader. Version 0.19.1
-v, --version Show version Usage:
-h, --help Show help
-c PATH, --config=PATH Path to the config file. Default is `~/.config/mango/config.yml` mango [sub_command] [options]
Options:
-c PATH, --config=PATH Path to the config file [type:String]
-h, --help Show this help.
-v, --version Show version.
Sub Commands:
admin Run admin tools
``` ```
### Config ### Config
@@ -60,24 +76,37 @@ The default config file location is `~/.config/mango/config.yml`. It might be di
```yaml ```yaml
--- ---
port: 9000 port: 9000
base_url: /
session_secret: mango-session-secret
library_path: ~/mango/library library_path: ~/mango/library
db_path: ~/mango/mango.db db_path: ~/mango/mango.db
scan_interval_minutes: 5 scan_interval_minutes: 5
thumbnail_generation_interval_hours: 24
db_optimization_interval_hours: 24
log_level: info log_level: info
upload_path: ~/mango/uploads
plugin_path: ~/mango/plugins
download_timeout_seconds: 30
page_margin: 30
disable_login: false
default_username: ""
mangadex: mangadex:
base_url: https://mangadex.org base_url: https://mangadex.org
api_url: https://mangadex.org/api api_url: https://mangadex.org/api
download_wait_seconds: 5 download_wait_seconds: 5
download_retries: 4 download_retries: 4
download_queue_db_path: ~/mango/queue.db download_queue_db_path: ~/mango/queue.db
chapter_rename_rule: '[Vol.{volume} ][Ch.{chapter} ]{title|id}'
manga_rename_rule: '{title}'
``` ```
- `scan_interval_minutes` can be any non-negative integer. Setting it to `0` disables the periodic scan - `scan_interval_minutes`, `thumbnail_generation_interval_hours` and `db_optimization_interval_hours` can be any non-negative integer. Setting them to `0` disables the periodic tasks
- `log_level` can be `debug`, `info`, `warn`, `error`, `fatal` or `off`. Setting it to `off` disables the logging - `log_level` can be `debug`, `info`, `warn`, `error`, `fatal` or `off`. Setting it to `off` disables the logging
- You can disable authentication by setting `disable_login` to true. Note that `default_username` must be set to an existing username for this to work.
### Library Structure ### Library Structure
You can organize your `.cbz/.zip` files in nested folders in the library directory. Here's an example: You can organize your archive files in nested folders in the library directory. Here's an example:
``` ```
. .
@@ -120,6 +149,34 @@ Mobile UI:
![mobile screenshot](./.github/screenshots/mobile.png) ![mobile screenshot](./.github/screenshots/mobile.png)
## Sponsors
<a href="https://casinoshunter.com/online-casinos/"><img src="https://i.imgur.com/EJb3wBo.png" width="150" height="auto"></a>
<a href="https://www.browserstack.com/open-source"><img src="https://i.imgur.com/hGJUJXD.png" width="150" height="auto"></a>
## Contributors ## Contributors
[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/0)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/1)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/2)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/3)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/4)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/5)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/6)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/7)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7) Please check the [development guideline](https://github.com/hkalexling/Mango/wiki/Development) if you are interested in code contributions.
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/hkalexling/"><img src="https://avatars1.githubusercontent.com/u/7845831?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Ling</b></sub></a><br /><a href="https://github.com/hkalexling/Mango/commits?author=hkalexling" title="Code">💻</a> <a href="https://github.com/hkalexling/Mango/commits?author=hkalexling" title="Documentation">📖</a> <a href="#infra-hkalexling" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/jaredlt"><img src="https://avatars1.githubusercontent.com/u/8590311?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jaredlt</b></sub></a><br /><a href="https://github.com/hkalexling/Mango/commits?author=jaredlt" title="Code">💻</a> <a href="#ideas-jaredlt" title="Ideas, Planning, & Feedback">🤔</a> <a href="#design-jaredlt" title="Design">🎨</a></td>
<td align="center"><a href="https://windisco.com/"><img src="https://avatars1.githubusercontent.com/u/4946624?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ココロ</b></sub></a><br /><a href="#infra-shincurry" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://catgirlsin.space/"><img src="https://avatars0.githubusercontent.com/u/13433513?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Valentijn</b></sub></a><br /><a href="#infra-noirscape" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/flying-sausages"><img src="https://avatars1.githubusercontent.com/u/23618693?v=4?s=100" width="100px;" alt=""/><br /><sub><b>flying-sausages</b></sub></a><br /><a href="https://github.com/hkalexling/Mango/commits?author=flying-sausages" title="Documentation">📖</a> <a href="#ideas-flying-sausages" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/XavierSchiller"><img src="https://avatars1.githubusercontent.com/u/22575255?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Xavier</b></sub></a><br /><a href="#infra-XavierSchiller" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/WROIATE"><img src="https://avatars3.githubusercontent.com/u/44677306?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jarao</b></sub></a><br /><a href="#infra-WROIATE" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Leeingnyo"><img src="https://avatars0.githubusercontent.com/u/6760150?v=4?s=100" width="100px;" alt=""/><br /><sub><b>이인용</b></sub></a><br /><a href="https://github.com/hkalexling/Mango/commits?author=Leeingnyo" title="Code">💻</a></td>
<td align="center"><a href="http://h45h74x.eu.org"><img src="https://avatars1.githubusercontent.com/u/27204033?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon</b></sub></a><br /><a href="https://github.com/hkalexling/Mango/commits?author=h45h74x" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
[ ! -z "$(grep '.\{80\}' --exclude-dir=lib --include="*.cr" -nr --color=always . | grep -v "routes/api.cr" | tee /dev/tty)" ] \
&& echo "The above lines exceed the 80 characters limit" \
|| exit 0
@@ -7,9 +7,9 @@ services:
context: . context: .
dockerfile: ./Dockerfile dockerfile: ./Dockerfile
expose: expose:
- 9000 - ${PORT}
ports: ports:
- 9000:9000 - "${PORT}:9000"
volumes: volumes:
- ~/mango:/root/mango - ${MAIN_DIRECTORY_PATH}:/root/mango
- ~/.config/mango:/root/.config/mango - ${CONFIG_DIRECTORY_PATH}:/root/.config/mango
+10
View File
@@ -0,0 +1,10 @@
# Port that exposes the HTTP frontend
PORT=9000
# Path to the mango main directory
# This directory holds the database and the library files
MAIN_DIRECTORY_PATH=
# Path to the mango config directory
# This directory holds the mango configuration path
CONFIG_DIRECTORY_PATH=
+53 -12
View File
@@ -1,29 +1,70 @@
const gulp = require('gulp'); const gulp = require('gulp');
const minify = require("gulp-babel-minify"); const babel = require('gulp-babel');
const minify = require('gulp-babel-minify');
const minifyCss = require('gulp-minify-css'); const minifyCss = require('gulp-minify-css');
const less = require('gulp-less');
gulp.task('minify-js', () => { // Copy libraries from node_moduels to public/js
return gulp.src('public/js/*.js') gulp.task('copy-js', () => {
return gulp.src([
'node_modules/@fortawesome/fontawesome-free/js/fontawesome.min.js',
'node_modules/@fortawesome/fontawesome-free/js/solid.min.js',
'node_modules/uikit/dist/js/uikit.min.js',
'node_modules/uikit/dist/js/uikit-icons.min.js'
])
.pipe(gulp.dest('public/js'));
});
// Copy UIKit SVG icons to public/img
gulp.task('copy-uikit-icons', () => {
return gulp.src('node_modules/uikit/src/images/backgrounds/*.svg')
.pipe(gulp.dest('public/img'));
});
// Compile less
gulp.task('less', () => {
return gulp.src('public/css/*.less')
.pipe(less())
.pipe(gulp.dest('public/css'));
});
// Transpile and minify JS files and output to dist
gulp.task('babel', () => {
return gulp.src(['public/js/*.js', '!public/js/*.min.js'])
.pipe(babel({
presets: [
['@babel/preset-env', {
targets: '>0.25%, not dead, ios>=9'
}]
],
}))
.pipe(minify({ .pipe(minify({
removeConsole: true removeConsole: true,
builtIns: false
})) }))
.pipe(gulp.dest('dist/js')); .pipe(gulp.dest('dist/js'));
}); });
// Minify CSS and output to dist
gulp.task('minify-css', () => { gulp.task('minify-css', () => {
return gulp.src('public/css/*.css') return gulp.src('public/css/*.css')
.pipe(minifyCss()) .pipe(minifyCss())
.pipe(gulp.dest('dist/css')); .pipe(gulp.dest('dist/css'));
}); });
gulp.task('img', () => { // Copy static files (includeing images) to dist
return gulp.src('public/img/*') gulp.task('copy-files', () => {
.pipe(gulp.dest('dist/img')); return gulp.src(['public/img/*', 'public/*.*', 'public/js/*.min.js'], {
}); base: 'public'
})
gulp.task('favicon', () => {
return gulp.src('public/favicon.ico')
.pipe(gulp.dest('dist')); .pipe(gulp.dest('dist'));
}); });
gulp.task('default', gulp.parallel('minify-js', 'minify-css', 'img', 'favicon')); // Set up the public folder for development
gulp.task('dev', gulp.parallel('copy-js', 'copy-uikit-icons', 'less'));
// Set up the dist folder for deployment
gulp.task('deploy', gulp.parallel('babel', 'minify-css', 'copy-files'));
// Default task
gulp.task('default', gulp.series('dev', 'deploy'));
+85
View File
@@ -0,0 +1,85 @@
class ForeignKeys < MG::Base
def up : String
<<-SQL
-- add foreign key to tags
ALTER TABLE tags RENAME TO tmp;
CREATE TABLE tags (
id TEXT NOT NULL,
tag TEXT NOT NULL,
UNIQUE (id, tag),
FOREIGN KEY (id) REFERENCES titles (id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
INSERT INTO tags
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE INDEX tags_id_idx ON tags (id);
CREATE INDEX tags_tag_idx ON tags (tag);
-- add foreign key to thumbnails
ALTER TABLE thumbnails RENAME TO tmp;
CREATE TABLE thumbnails (
id TEXT NOT NULL,
data BLOB NOT NULL,
filename TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL,
FOREIGN KEY (id) REFERENCES ids (id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
INSERT INTO thumbnails
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE UNIQUE INDEX tn_index ON thumbnails (id);
SQL
end
def down : String
<<-SQL
-- remove foreign key from thumbnails
ALTER TABLE thumbnails RENAME TO tmp;
CREATE TABLE thumbnails (
id TEXT NOT NULL,
data BLOB NOT NULL,
filename TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL
);
INSERT INTO thumbnails
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE UNIQUE INDEX tn_index ON thumbnails (id);
-- remove foreign key from tags
ALTER TABLE tags RENAME TO tmp;
CREATE TABLE tags (
id TEXT NOT NULL,
tag TEXT NOT NULL,
UNIQUE (id, tag)
);
INSERT INTO tags
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE INDEX tags_id_idx ON tags (id);
CREATE INDEX tags_tag_idx ON tags (tag);
SQL
end
end
+19
View File
@@ -0,0 +1,19 @@
class CreateIds < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS ids (
path TEXT NOT NULL,
id TEXT NOT NULL,
is_title INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS path_idx ON ids (path);
CREATE UNIQUE INDEX IF NOT EXISTS id_idx ON ids (id);
SQL
end
def down : String
<<-SQL
DROP TABLE ids;
SQL
end
end
+19
View File
@@ -0,0 +1,19 @@
class CreateTags < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS tags (
id TEXT NOT NULL,
tag TEXT NOT NULL,
UNIQUE (id, tag)
);
CREATE INDEX IF NOT EXISTS tags_id_idx ON tags (id);
CREATE INDEX IF NOT EXISTS tags_tag_idx ON tags (tag);
SQL
end
def down : String
<<-SQL
DROP TABLE tags;
SQL
end
end
+20
View File
@@ -0,0 +1,20 @@
class CreateThumbnails < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS thumbnails (
id TEXT NOT NULL,
data BLOB NOT NULL,
filename TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS tn_index ON thumbnails (id);
SQL
end
def down : String
<<-SQL
DROP TABLE thumbnails;
SQL
end
end
+56
View File
@@ -0,0 +1,56 @@
class CreateTitles < MG::Base
def up : String
<<-SQL
-- create titles
CREATE TABLE titles (
id TEXT NOT NULL,
path TEXT NOT NULL,
signature TEXT
);
CREATE UNIQUE INDEX titles_id_idx on titles (id);
CREATE UNIQUE INDEX titles_path_idx on titles (path);
-- migrate data from ids to titles
INSERT INTO titles
SELECT id, path, null
FROM ids
WHERE is_title = 1;
DELETE FROM ids
WHERE is_title = 1;
-- remove the is_title column from ids
ALTER TABLE ids RENAME TO tmp;
CREATE TABLE ids (
path TEXT NOT NULL,
id TEXT NOT NULL
);
INSERT INTO ids
SELECT path, id
FROM tmp;
DROP TABLE tmp;
-- recreate the indices
CREATE UNIQUE INDEX path_idx ON ids (path);
CREATE UNIQUE INDEX id_idx ON ids (id);
SQL
end
def down : String
<<-SQL
-- insert the is_title column
ALTER TABLE ids ADD COLUMN is_title INTEGER NOT NULL DEFAULT 0;
-- migrate data from titles to ids
INSERT INTO ids
SELECT path, id, 1
FROM titles;
-- remove titles
DROP TABLE titles;
SQL
end
end
+20
View File
@@ -0,0 +1,20 @@
class CreateUsers < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
password TEXT NOT NULL,
token TEXT,
admin INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS username_idx ON users (username);
CREATE UNIQUE INDEX IF NOT EXISTS token_idx ON users (token);
SQL
end
def down : String
<<-SQL
DROP TABLE users;
SQL
end
end
+23 -14
View File
@@ -1,16 +1,25 @@
{ {
"name": "mango", "name": "mango",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/hkalexling/Mango.git", "repository": "https://github.com/hkalexling/Mango.git",
"author": "Alex Ling <hkalexling@gmail.com>", "author": "Alex Ling <hkalexling@gmail.com>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"gulp": "^4.0.2", "@babel/preset-env": "^7.11.5",
"gulp-babel-minify": "^0.5.1", "all-contributors-cli": "^6.19.0",
"gulp-minify-css": "^1.2.4" "gulp": "^4.0.2",
}, "gulp-babel": "^8.0.0",
"scripts": { "gulp-babel-minify": "^0.5.1",
"uglify": "gulp" "gulp-less": "^4.0.1",
} "gulp-minify-css": "^1.2.4",
"less": "^3.11.3"
},
"scripts": {
"uglify": "gulp"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0",
"uikit": "^3.5.4"
}
} }
-46
View File
@@ -1,46 +0,0 @@
.uk-alert-close {
color: black !important;
}
.uk-card-body {
padding: 20px;
}
.uk-card-media-top {
max-height: 350px;
overflow: hidden;
}
.acard:hover {
text-decoration: none;
}
.uk-list li {
cursor: pointer;
}
.reader-bg {
background-color: black;
}
#scan-status {
cursor: auto;
}
.break-word {
word-wrap: break-word;
}
.uk-logo > img {
max-height: 90px;
}
.uk-search {
width: 100%;
}
#selectable .ui-selecting {
background: #EEE6B9;
}
#selectable .ui-selected {
background: #F4E487;
}
#selectable .ui-selecting.dark {
background: #5E5731;
}
#selectable .ui-selected.dark {
background: #9D9252;
}
td > .uk-dropdown {
white-space: pre-line;
}
+124
View File
@@ -0,0 +1,124 @@
// Item cards
.item .uk-card {
cursor: pointer;
.uk-card-media-top {
width: 100%;
height: 250px;
@media (min-width: 600px) {
height: 300px;
}
img {
height: 100%;
width: 100%;
object-fit: cover;
&.grayscale {
filter: grayscale(100%);
}
}
}
.uk-card-body {
padding: 20px;
.uk-card-title {
max-height: 3em;
font-size: 1rem;
}
}
}
// jQuery selectable
#selectable {
.ui-selecting {
background: #EEE6B9;
}
.ui-selected {
background: #F4E487;
}
.uk-light & {
.ui-selecting {
background: #5E5731;
}
.ui-selected {
background: #9D9252;
}
}
}
// Edit modal
#edit-modal {
.uk-grid > div {
height: 300px;
}
#cover {
height: 100%;
width: 100%;
object-fit: cover;
}
#cover-upload {
height: 100%;
box-sizing: border-box;
}
.uk-modal-body .uk-inline {
width: 100%;
}
}
// Dark theme
.uk-light {
.uk-navbar-dropdown,
.uk-modal-header,
.uk-modal-body,
.uk-modal-footer {
background: #222;
}
.uk-navbar-dropdown,
.uk-dropdown {
color: #ccc;
}
.uk-nav-header,
.uk-description-list > dt {
color: #555;
}
}
// Alpine magic
[x-cloak] {
display: none;
}
// Batch select bar on title page
#select-bar-controls {
a {
transform: scale(1.5, 1.5);
&:hover {
color: orange;
}
}
}
// Totop button
#totop-wrapper {
position: absolute;
top: 100vh;
right: 2em;
bottom: 0;
a {
position: fixed;
position: sticky;
top: calc(100vh - 5em);
}
}
// Misc
.uk-alert-close {
color: black !important;
}
.break-word {
word-wrap: break-word;
}
.uk-search {
width: 100%;
}
+58
View File
@@ -0,0 +1,58 @@
@light-gray: #e5e5e5;
@gray: #666666;
@black: #141414;
@blue: rgb(30, 135, 240);
@white1: rgba(255, 255, 255, .1);
@white2: rgba(255, 255, 255, .2);
@white7: rgba(255, 255, 255, .7);
.select2-container--default {
.select2-selection--multiple {
border: 1px solid @light-gray;
.select2-selection__choice,
.select2-selection__choice__remove,
.select2-selection__choice__remove:hover
{
background-color: @blue;
color: white;
border: none;
border-radius: 2px;
}
}
.select2-dropdown {
.select2-results__option--highlighted.select2-results__option--selectable {
background-color: @blue;
}
.select2-results__option--selected:not(.select2-results__option--highlighted) {
background-color: @light-gray
}
}
}
.uk-light {
.select2-container--default {
.select2-selection {
background-color: @white1;
}
.select2-selection--multiple {
border: 1px solid @white2;
.select2-selection__choice,
.select2-selection__choice__remove,
.select2-selection__choice__remove:hover
{
background-color: white;
color: @gray;
border: none;
}
.select2-search__field {
color: @white7;
}
}
}
.select2-dropdown {
background-color: @black;
.select2-results__option--selected:not(.select2-results__option--highlighted) {
background-color: @white2;
}
}
}
+45
View File
@@ -0,0 +1,45 @@
@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;
}
@internal-divider-icon-image: "../img/divider-icon.svg";
@internal-form-select-image: "../img/form-select.svg";
@internal-form-datalist-image: "../img/form-datalist.svg";
@internal-form-radio-image: "../img/form-radio.svg";
@internal-form-checkbox-image: "../img/form-checkbox.svg";
@internal-form-checkbox-indeterminate-image: "../img/form-checkbox-indeterminate.svg";
@internal-nav-parent-close-image: "../img/nav-parent-close.svg";
@internal-nav-parent-open-image: "../img/nav-parent-open.svg";
@internal-list-bullet-image: "../img/list-bullet.svg";
@internal-accordion-open-image: "../img/accordion-open.svg";
@internal-accordion-close-image: "../img/accordion-close.svg";
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

+55 -25
View File
@@ -1,25 +1,55 @@
var scanning = false; const component = () => {
function scan() { return {
scanning = true; progress: 1.0,
$('#scan-status > div').removeAttr('hidden'); generating: false,
$('#scan-status > span').attr('hidden', ''); scanning: false,
var color = $('#scan').css('color'); scanTitles: 0,
$('#scan').css('color', 'gray'); scanMs: -1,
$.post('/api/admin/scan', function (data) { themeSetting: '',
var ms = data.milliseconds;
var titles = data.titles; init() {
$('#scan-status > span').text('Scanned ' + titles + ' titles in ' + ms + 'ms'); this.getProgress();
$('#scan-status > span').removeAttr('hidden'); setInterval(() => {
$('#scan').css('color', color); this.getProgress();
$('#scan-status > div').attr('hidden', ''); }, 5000);
scanning = false;
}); const setting = loadThemeSetting();
} this.themeSetting = setting.charAt(0).toUpperCase() + setting.slice(1);
$(function() { },
$('li').click(function() { themeChanged(event) {
url = $(this).attr('data-url'); const newSetting = $(event.currentTarget).val().toLowerCase();
if (url) { saveThemeSetting(newSetting);
$(location).attr('href', url); setTheme();
} },
}); scan() {
}); if (this.scanning) return;
this.scanning = true;
this.scanMs = -1;
this.scanTitles = 0;
$.post(`${base_url}api/admin/scan`)
.then(data => {
this.scanMs = data.milliseconds;
this.scanTitles = data.titles;
})
.always(() => {
this.scanning = false;
});
},
generateThumbnails() {
if (this.generating) return;
this.generating = true;
this.progress = 0.0;
$.post(`${base_url}api/admin/generate_thumbnails`)
.then(() => {
this.getProgress()
});
},
getProgress() {
$.get(`${base_url}api/admin/thumbnail_progress`)
.then(data => {
this.progress = data.progress;
this.generating = data.progress > 0;
});
},
};
};
+147
View File
@@ -0,0 +1,147 @@
/**
* --- Alpine helper functions
*/
/**
* Set an alpine.js property
*
* @function setProp
* @param {string} key - Key of the data property
* @param {*} prop - The data property
* @param {string} selector - The jQuery selector to the root element
*/
const setProp = (key, prop, selector = '#root') => {
$(selector).get(0).__x.$data[key] = prop;
};
/**
* Get an alpine.js property
*
* @function getProp
* @param {string} key - Key of the data property
* @param {string} selector - The jQuery selector to the root element
* @return {*} The data property
*/
const getProp = (key, selector = '#root') => {
return $(selector).get(0).__x.$data[key];
};
/**
* --- Theme related functions
* Note: In the comments below we treat "theme" and "theme setting"
* differently. A theme can have only two values, either "dark" or
* "light", while a theme setting can have the third value "system".
*/
/**
* Check if the system setting prefers dark theme.
* from https://flaviocopes.com/javascript-detect-dark-mode/
*
* @function preferDarkMode
* @return {bool}
*/
const preferDarkMode = () => {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
};
/**
* Check whether a given string represents a valid theme setting
*
* @function validThemeSetting
* @param {string} theme - The string representing the theme setting
* @return {bool}
*/
const validThemeSetting = (theme) => {
return ['dark', 'light', 'system'].indexOf(theme) >= 0;
};
/**
* Load theme setting from local storage, or use 'light'
*
* @function loadThemeSetting
* @return {string} A theme setting ('dark', 'light', or 'system')
*/
const loadThemeSetting = () => {
let str = localStorage.getItem('theme');
if (!str || !validThemeSetting(str)) str = 'system';
return str;
};
/**
* Load the current theme (not theme setting)
*
* @function loadTheme
* @return {string} The current theme to use ('dark' or 'light')
*/
const loadTheme = () => {
let setting = loadThemeSetting();
if (setting === 'system') {
setting = preferDarkMode() ? 'dark' : 'light';
}
return setting;
};
/**
* Save a theme setting
*
* @function saveThemeSetting
* @param {string} setting - A theme setting
*/
const saveThemeSetting = setting => {
if (!validThemeSetting(setting)) setting = 'system';
localStorage.setItem('theme', setting);
};
/**
* Toggle the current theme. When the current theme setting is 'system', it
* will be changed to either 'light' or 'dark'
*
* @function toggleTheme
*/
const toggleTheme = () => {
const theme = loadTheme();
const newTheme = theme === 'dark' ? 'light' : 'dark';
saveThemeSetting(newTheme);
setTheme(newTheme);
};
/**
* Apply a theme, or load a theme and then apply it
*
* @function setTheme
* @param {string?} theme - (Optional) The theme to apply. When omitted, use
* `loadTheme` to get a theme and apply it.
*/
const setTheme = (theme) => {
if (!theme) theme = loadTheme();
if (theme === 'dark') {
$('html').css('background', 'rgb(20, 20, 20)');
$('body').addClass('uk-light');
$('.uk-card').addClass('uk-card-secondary');
$('.uk-card').removeClass('uk-card-default');
$('.ui-widget-content').addClass('dark');
} else {
$('html').css('background', '');
$('body').removeClass('uk-light');
$('.uk-card').removeClass('uk-card-secondary');
$('.uk-card').addClass('uk-card-default');
$('.ui-widget-content').removeClass('dark');
}
};
// do it before document is ready to prevent the initial flash of white on
// most pages
setTheme();
$(() => {
// hack for the reader page
setTheme();
// on system dark mode setting change
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', event => {
if (loadThemeSetting() === 'system')
setTheme(event.matches ? 'dark' : 'light');
});
}
});
+26
View File
@@ -0,0 +1,26 @@
/**
* Truncate a .uk-card-title element
*
* @function truncate
* @param {object} e - The title element to truncate
*/
const truncate = (e) => {
$(e).dotdotdot({
truncate: 'letter',
watch: true,
callback: (truncated) => {
if (truncated) {
$(e).attr('uk-tooltip', $(e).attr('data-title'));
} else {
$(e).removeAttr('uk-tooltip');
}
}
});
};
$('.uk-card-title').each((i, e) => {
// Truncate the title when it first enters the view
$(e).one('inview', () => {
truncate(e);
});
});
+113 -135
View File
@@ -1,138 +1,116 @@
$(() => { const component = () => {
$('input.uk-checkbox').each((i, e) => { return {
$(e).change(() => { jobs: [],
loadConfig(); paused: undefined,
}); loading: false,
}); toggling: false,
loadConfig(); ws: undefined,
load();
const intervalMS = 5000; wsConnect(secure = true) {
setTimeout(() => { const url = `${secure ? 'wss' : 'ws'}://${location.host}${base_url}api/admin/mangadex/queue`;
setInterval(() => { console.log(`Connecting to ${url}`);
if (globalConfig.autoRefresh !== true) return; this.ws = new WebSocket(url);
load(); this.ws.onmessage = event => {
}, intervalMS); const data = JSON.parse(event.data);
}, intervalMS); this.jobs = data.jobs;
}); this.paused = data.paused;
var globalConfig = {}; };
var loading = false; this.ws.onclose = () => {
if (this.ws.failed)
const loadConfig = () => { return this.wsConnect(false);
globalConfig.autoRefresh = $('#auto-refresh').prop('checked'); alert('danger', 'Socket connection closed');
}; };
const remove = (id) => { this.ws.onerror = () => {
var url = '/api/admin/mangadex/queue/delete'; if (secure)
if (id !== undefined) return this.ws.failed = true;
url += '?' + $.param({id: id}); alert('danger', 'Socket connection failed');
console.log(url); };
$.ajax({ },
type: 'POST', init() {
url: url, this.wsConnect();
dataType: 'json' this.load();
}) },
.done(data => { load() {
if (!data.success && data.error) { this.loading = true;
alert('danger', `Failed to remove job from download queue. Error: ${data.error}`); $.ajax({
return; 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;
}
this.jobs = data.jobs;
this.paused = data.paused;
})
.fail((jqXHR, status) => {
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
this.loading = false;
});
},
jobAction(action, event) {
let url = `${base_url}api/admin/mangadex/queue/${action}`;
if (event) {
const id = event.currentTarget.closest('tr').id.split('-')[1];
url = `${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 ${action} job from download queue. Error: ${data.error}`);
return;
}
this.load();
})
.fail((jqXHR, status) => {
alert('danger', `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
},
toggle() {
this.toggling = true;
const action = this.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(() => {
this.load();
this.toggling = false;
});
},
statusClass(status) {
let cls = 'label ';
switch (status) {
case 'Pending':
cls += 'label-pending';
break;
case 'Completed':
cls += 'label-success';
break;
case 'Error':
cls += 'label-danger';
break;
case 'MissingPages':
cls += 'label-warning';
break;
}
return cls;
} }
load(); };
})
.fail((jqXHR, status) => {
alert('danger', `Failed to remove job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
};
const refresh = (id) => {
var url = '/api/admin/mangadex/queue/retry';
if (id !== undefined)
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}`);
});
};
const toggle = () => {
$('#pause-resume-btn').attr('disabled', '');
const paused = $('#pause-resume-btn').text() === 'Resume download';
const action = paused ? 'resume' : 'pause';
const 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');
});
};
const load = () => {
if (loading) return;
loading = true;
console.log('fetching');
$.ajax({
type: 'GET',
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';
const info = obj.status_message.length > 0 ? '<span uk-icon="info"></span>' : '';
const statusSpan = `<span class="${cls}">${obj.status} ${info}</span>`;
const dropdown = obj.status_message.length > 0 ? `<div uk-dropdown>${obj.status_message}</div>` : '';
const retryBtn = obj.status_message.length > 0 ? `<a onclick="refresh('${obj.id}')" uk-icon="refresh"></a>` : '';
return `<tr id="chapter-${obj.id}">
<td><a href="${baseURL}/chapter/${obj.id}">${obj.title}</a></td>
<td><a href="${baseURL}/manga/${obj.manga_id}">${obj.manga_title}</a></td>
<td>${obj.success_count}/${obj.pages}</td>
<td>${moment(obj.time).fromNow()}</td>
<td>${statusSpan} ${dropdown}</td>
<td>
<a onclick="remove('${obj.id}')" uk-icon="trash"></a>
${retryBtn}
</td>
</tr>`;
});
const tbody = `<tbody>${rows.join('')}</tbody>`;
$('tbody').remove();
$('table').append(tbody);
})
.fail((jqXHR, status) => {
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
loading = false;
});
}; };
+51 -45
View File
@@ -32,42 +32,41 @@ const download = () => {
const chapters = globalChapters.filter(c => ids.indexOf(c.id) >= 0); const chapters = globalChapters.filter(c => ids.indexOf(c.id) >= 0);
console.log(ids); console.log(ids);
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/admin/mangadex/download', url: base_url + 'api/admin/mangadex/download',
data: JSON.stringify({chapters: chapters}), data: JSON.stringify({
contentType: "application/json", chapters: chapters
dataType: 'json' }),
}) contentType: "application/json",
.done(data => { dataType: 'json'
console.log(data); })
if (data.error) { .done(data => {
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`); console.log(data);
return; if (data.error) {
} alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
const successCount = parseInt(data.success); return;
const failCount = parseInt(data.fail); }
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => { const successCount = parseInt(data.success);
window.location.href = '/admin/downloads'; const failCount = parseInt(data.fail);
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
window.location.href = base_url + 'admin/downloads';
});
})
.fail((jqXHR, status) => {
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
$('#download-spinner').attr('hidden', '');
$('#download-btn').removeAttr('hidden');
}); });
styleModal();
})
.fail((jqXHR, status) => {
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
$('#download-spinner').attr('hidden', '');
$('#download-btn').removeAttr('hidden');
});
}); });
styleModal();
}; };
const toggleSpinner = () => { const toggleSpinner = () => {
var attr = $('#spinner').attr('hidden'); var attr = $('#spinner').attr('hidden');
if (attr) { if (attr) {
$('#spinner').removeAttr('hidden'); $('#spinner').removeAttr('hidden');
$('#search-btn').attr('hidden', ''); $('#search-btn').attr('hidden', '');
} } else {
else {
$('#search-btn').removeAttr('hidden'); $('#search-btn').removeAttr('hidden');
$('#spinner').attr('hidden', ''); $('#spinner').attr('hidden', '');
} }
@@ -96,10 +95,9 @@ const search = () => {
try { try {
const path = new URL(input).pathname; const path = new URL(input).pathname;
const match = /\/title\/([0-9]+)/.exec(path); const match = /\/(?:title|manga)\/([0-9]+)/.exec(path);
int_id = parseInt(match[1]); int_id = parseInt(match[1]);
} } catch (e) {
catch(e) {
int_id = parseInt(input); int_id = parseInt(input);
} }
@@ -109,7 +107,7 @@ const search = () => {
return; return;
} }
$.getJSON("/api/admin/mangadex/manga/" + int_id) $.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`)
.done((data) => { .done((data) => {
if (data.error) { if (data.error) {
alert('danger', 'Failed to get manga info. Error: ' + data.error); alert('danger', 'Failed to get manga info. Error: ' + data.error);
@@ -139,8 +137,12 @@ const search = () => {
const comp = (a, b) => { const comp = (a, b) => {
var ai; var ai;
var bi; var bi;
try {ai = parseFloat(a);} catch(e) {} try {
try {bi = parseFloat(b);} catch(e) {} ai = parseFloat(a);
} catch (e) {}
try {
bi = parseFloat(b);
} catch (e) {}
if (typeof ai === 'undefined') return -1; if (typeof ai === 'undefined') return -1;
if (typeof bi === 'undefined') return 1; if (typeof bi === 'undefined') return 1;
if (ai < bi) return 1; if (ai < bi) return 1;
@@ -176,8 +178,7 @@ const parseRange = str => {
if (!matches) { if (!matches) {
alert('danger', `Failed to parse filter input ${str}`); alert('danger', `Failed to parse filter input ${str}`);
return [null, null]; return [null, null];
} } else if (typeof matches[1] !== 'undefined' && typeof matches[2] !== 'undefined') {
else if (typeof matches[1] !== 'undefined' && typeof matches[2] !== 'undefined') {
// e.g., <= 30 // e.g., <= 30
num = parseInt(matches[2]); num = parseInt(matches[2]);
if (isNaN(num)) { if (isNaN(num)) {
@@ -194,8 +195,7 @@ const parseRange = str => {
case '>=': case '>=':
return [num, null]; return [num, null];
} }
} } else if (typeof matches[3] !== 'undefined') {
else if (typeof matches[3] !== 'undefined') {
// a single number // a single number
num = parseInt(matches[3]); num = parseInt(matches[3]);
if (isNaN(num)) { if (isNaN(num)) {
@@ -203,8 +203,7 @@ const parseRange = str => {
return [null, null]; return [null, null];
} }
return [num, num]; return [num, num];
} } else if (typeof matches[4] !== 'undefined' && typeof matches[5] !== 'undefined') {
else if (typeof matches[4] !== 'undefined' && typeof matches[5] !== 'undefined') {
// e.g., 10 - 23 // e.g., 10 - 23
num = parseInt(matches[4]); num = parseInt(matches[4]);
const n2 = parseInt(matches[5]); const n2 = parseInt(matches[5]);
@@ -213,8 +212,7 @@ const parseRange = str => {
return [null, null]; return [null, null];
} }
return [num, n2]; return [num, n2];
} } else {
else {
// empty or space only // empty or space only
return [null, null]; return [null, null];
} }
@@ -242,7 +240,10 @@ const buildTable = () => {
Object.entries(filters).forEach(([k, v]) => { Object.entries(filters).forEach(([k, v]) => {
if (v === 'All') return; if (v === 'All') return;
if (k === 'group') { if (k === 'group') {
chapters = chapters.filter(c => v in c.groups); chapters = chapters.filter(c => {
unescaped_groups = Object.entries(c.groups).map(([g, id]) => unescapeHTML(g));
return unescaped_groups.indexOf(v) >= 0;
});
return; return;
} }
if (k === 'lang') { if (k === 'lang') {
@@ -277,8 +278,7 @@ const buildTable = () => {
const group_str = Object.entries(chp.groups).map(([k, v]) => { const group_str = Object.entries(chp.groups).map(([k, v]) => {
return `<a href="${baseURL }/group/${v}">${k}</a>`; return `<a href="${baseURL }/group/${v}">${k}</a>`;
}).join(' | '); }).join(' | ');
const dark = getTheme() === 'dark' ? 'dark' : ''; return `<tr class="ui-widget-content">
return `<tr class="ui-widget-content ${dark}">
<td><a href="${baseURL}/chapter/${chp.id}">${chp.id}</a></td> <td><a href="${baseURL}/chapter/${chp.id}">${chp.id}</a></td>
<td>${chp.title}</td> <td>${chp.title}</td>
<td>${chp.language}</td> <td>${chp.language}</td>
@@ -297,3 +297,9 @@ const buildTable = () => {
}); });
$('#selection-controls').removeAttr('hidden'); $('#selection-controls').removeAttr('hidden');
}; };
const unescapeHTML = (str) => {
var elt = document.createElement("span");
elt.innerHTML = str;
return elt.innerText;
};
-5
View File
File diff suppressed because one or more lines are too long
+141
View File
@@ -0,0 +1,141 @@
const loadPlugin = id => {
localStorage.setItem('plugin', id);
const url = `${location.protocol}//${location.host}${location.pathname}`;
const newURL = `${url}?${$.param({
plugin: id
})}`;
window.location.href = newURL;
};
$(() => {
var storedID = localStorage.getItem('plugin');
if (storedID && storedID !== pid) {
loadPlugin(storedID);
} else {
$('#controls').removeAttr('hidden');
}
$('#search-input').keypress(event => {
if (event.which === 13) {
search();
}
});
$('#plugin-select').val(pid);
$('#plugin-select').change(() => {
const id = $('#plugin-select').val();
loadPlugin(id);
});
});
let mangaTitle = "";
let searching = false;
const search = () => {
if (searching)
return;
const query = $.param({
query: $('#search-input').val(),
plugin: pid
});
$.ajax({
type: 'GET',
url: `${base_url}api/admin/plugin/list?${query}`,
contentType: "application/json",
dataType: 'json'
})
.done(data => {
console.log(data);
if (data.error) {
alert('danger', `Search failed. Error: ${data.error}`);
return;
}
mangaTitle = data.title;
$('#title-text').text(data.title);
buildTable(data.chapters);
})
.fail((jqXHR, status) => {
alert('danger', `Search failed. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {});
};
const buildTable = (chapters) => {
$('#table').attr('hidden', '');
$('table').empty();
const keys = Object.keys(chapters[0]).map(k => `<th>${k}</th>`).join('');
const thead = `<thead><tr>${keys}</tr></thead>`;
$('table').append(thead);
const rows = chapters.map(ch => {
const tds = Object.values(ch).map(v => `<td>${v}</td>`).join('');
return `<tr data-id="${ch.id}" data-title="${ch.title}">${tds}</tr>`;
});
const tbody = `<tbody id="selectable">${rows}</tbody>`;
$('table').append(tbody);
$('#selectable').selectable({
filter: 'tr'
});
$('#table table').tablesorter();
$('#table').removeAttr('hidden');
};
const selectAll = () => {
$('tbody > tr').each((i, e) => {
$(e).addClass('ui-selected');
});
};
const unselect = () => {
$('tbody > tr').each((i, e) => {
$(e).removeClass('ui-selected');
});
};
const download = () => {
const selected = $('tbody > tr.ui-selected');
if (selected.length === 0) return;
UIkit.modal.confirm(`Download ${selected.length} selected chapters?`).then(() => {
$('#download-btn').attr('hidden', '');
$('#download-spinner').removeAttr('hidden');
const chapters = selected.map((i, e) => {
return {
id: $(e).attr('data-id'),
title: $(e).attr('data-title')
}
}).get();
console.log(chapters);
$.ajax({
type: 'POST',
url: base_url + 'api/admin/plugin/download',
data: JSON.stringify({
plugin: pid,
chapters: chapters,
title: mangaTitle
}),
contentType: "application/json",
dataType: 'json'
})
.done(data => {
console.log(data);
if (data.error) {
alert('danger', `Failed to add chapters to the download queue. Error: ${data.error}`);
return;
}
const successCount = parseInt(data.success);
const failCount = parseInt(data.fail);
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
window.location.href = base_url + 'admin/downloads';
});
})
.fail((jqXHR, status) => {
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
$('#download-spinner').attr('hidden', '');
$('#download-btn').removeAttr('hidden');
});
});
};
+273 -73
View File
@@ -1,81 +1,281 @@
$(function() { const readerComponent = () => {
function bind() { return {
var controller = new ScrollMagic.Controller(); loading: true,
mode: 'continuous', // Can be 'continuous', 'height' or 'width'
msg: 'Loading the web reader. Please wait...',
alertClass: 'uk-alert-primary',
items: [],
curItem: {},
flipAnimation: null,
longPages: false,
lastSavedPage: page,
// replace history on scroll /**
$('img').each(function(idx){ * Initialize the component by fetching the page dimensions
var scene = new ScrollMagic.Scene({ */
triggerElement: $(this).get(), init(nextTick) {
triggerHook: 'onEnter', $.get(`${base_url}api/dimensions/${tid}/${eid}`)
reverse: true .then(data => {
}) if (!data.success && data.error)
.addTo(controller) throw new Error(resp.error);
.on('enter', function(event){ const dimensions = data.dimensions;
current = $(event.target.triggerElement()).attr('id');
replaceHistory(current); this.items = dimensions.map((d, i) => {
return {
id: i + 1,
url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
width: d.width,
height: d.height,
style: `margin-top: ${data.margin}px; margin-bottom: ${data.margin}px;`
};
});
const avgRatio = this.items.reduce((acc, cur) => {
return acc + cur.height / cur.width
}, 0) / this.items.length;
console.log(avgRatio);
this.longPages = avgRatio > 2;
this.loading = false;
this.mode = localStorage.getItem('mode') || 'continuous';
// Here we save a copy of this.mode, and use the copy as
// the model-select value. This is because `updateMode`
// might change this.mode and make it `height` or `width`,
// which are not available in mode-select
const mode = this.mode;
this.updateMode(this.mode, page, nextTick);
$('#mode-select').val(mode);
}) })
.on('leave', function(event){ .catch(e => {
var prev = $(event.target.triggerElement()).prev(); const errMsg = `Failed to get the page dimensions. ${e}`;
current = $(prev).attr('id'); console.error(e);
replaceHistory(current); this.alertClass = 'uk-alert-danger';
}); this.msg = errMsg;
}); })
},
/**
* Handles the `change` event for the page selector
*/
pageChanged() {
const p = parseInt($('#page-select').val());
this.toPage(p);
},
/**
* Handles the `change` event for the mode selector
*
* @param {function} nextTick - Alpine $nextTick magic property
*/
modeChanged(nextTick) {
const mode = $('#mode-select').val();
const curIdx = parseInt($('#page-select').val());
// poor man's infinite scroll this.updateMode(mode, curIdx, nextTick);
var scene = new ScrollMagic.Scene({ },
triggerElement: $('.next-url').get(), /**
triggerHook: 'onEnter', * Handles the window `resize` event
offset: -500 */
}) resized() {
.addTo(controller) if (this.mode === 'continuous') return;
.on('enter', function(){
var nextURL = $('.next-url').attr('href'); const wideScreen = $(window).width() > $(window).height();
$('.next-url').remove(); this.mode = wideScreen ? 'height' : 'width';
if (!nextURL) { },
console.log('No .next-url found. Reached end of page'); /**
var lastURL = $('img').last().attr('id'); * Handles the window `keydown` event
// load the reader URL for the last page to update reading progrss to 100% *
$.get(lastURL); * @param {Event} event - The triggering event
$('#next-btn').removeAttr('hidden'); */
return; keyHandler(event) {
if (this.mode === 'continuous') return;
if (event.key === 'ArrowLeft' || event.key === 'k')
this.flipPage(false);
if (event.key === 'ArrowRight' || event.key === 'j')
this.flipPage(true);
},
/**
* Flips to the next or the previous page
*
* @param {bool} isNext - Whether we are going to the next page
*/
flipPage(isNext) {
const idx = parseInt(this.curItem.id);
const newIdx = idx + (isNext ? 1 : -1);
if (newIdx <= 0 || newIdx > this.items.length) return;
this.toPage(newIdx);
if (isNext)
this.flipAnimation = 'right';
else
this.flipAnimation = 'left';
setTimeout(() => {
this.flipAnimation = null;
}, 500);
this.replaceHistory(newIdx);
},
/**
* Jumps to a specific page
*
* @param {number} idx - One-based index of the page
*/
toPage(idx) {
if (this.mode === 'continuous') {
$(`#${idx}`).get(0).scrollIntoView(true);
} else {
if (idx >= 1 && idx <= this.items.length) {
this.curItem = this.items[idx - 1];
} }
$('#hidden').load(encodeURI(nextURL) + ' .uk-container', function(res, status, xhr){ }
if (status === 'error') console.log(xhr.statusText); this.replaceHistory(idx);
if (status === 'success') { UIkit.modal($('#modal-sections')).hide();
console.log(nextURL + ' loaded'); },
// new page loaded to #hidden, we now append it /**
$('.uk-section > .uk-container').append($('#hidden .uk-container').children()); * Replace the address bar history and save the reading progress if necessary
$('#hidden').empty(); *
bind(); * @param {number} idx - One-based index of the page
*/
replaceHistory(idx) {
const ary = window.location.pathname.split('/');
ary[ary.length - 1] = idx;
ary.shift(); // remove leading `/`
ary.unshift(window.location.origin);
const url = ary.join('/');
this.saveProgress(idx);
history.replaceState(null, "", url);
},
/**
* Updates the backend reading progress if:
* 1) the current page is more than five pages away from the last
* saved page, or
* 2) the average height/width ratio of the pages is over 2, or
* 3) the current page is the first page, or
* 4) the current page is the last page
*
* @param {number} idx - One-based index of the page
* @param {function} cb - Callback
*/
saveProgress(idx, cb) {
idx = parseInt(idx);
if (Math.abs(idx - this.lastSavedPage) >= 5 ||
this.longPages ||
idx === 1 || idx === this.items.length
) {
this.lastSavedPage = idx;
console.log('saving progress', idx);
const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`;
$.ajax({
method: 'PUT',
url: url,
dataType: 'json'
})
.done(data => {
if (data.error)
alert('danger', data.error);
if (cb) cb();
})
.fail((jqXHR, status) => {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
}
},
/**
* Updates the reader mode
*
* @param {string} mode - Either `continuous` or `paged`
* @param {number} targetPage - The one-based index of the target page
* @param {function} nextTick - Alpine $nextTick magic property
*/
updateMode(mode, targetPage, nextTick) {
localStorage.setItem('mode', mode);
// The mode to be put into the `mode` prop. It can't be `screen`
let propMode = mode;
if (mode === 'paged') {
const wideScreen = $(window).width() > $(window).height();
propMode = wideScreen ? 'height' : 'width';
}
this.mode = propMode;
if (mode === 'continuous') {
nextTick(() => {
this.setupScroller();
});
}
nextTick(() => {
this.toPage(targetPage);
});
},
/**
* Shows the control modal
*
* @param {Event} event - The triggering event
*/
showControl(event) {
const idx = event.currentTarget.id;
const pageCount = this.items.length;
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
$('#progress-label').text(progressText);
$('#page-select').val(idx);
UIkit.modal($('#modal-sections')).show();
},
/**
* Redirects to a URL
*
* @param {string} url - The target URL
*/
redirect(url) {
window.location.replace(url);
},
/**
* Set up the scroll handler that calls `replaceHistory` when an image
* enters the view port
*/
setupScroller() {
if (this.mode !== 'continuous') return;
$('img').each((idx, el) => {
$(el).on('inview', (event, inView) => {
if (inView) {
const current = $(event.currentTarget).attr('id');
this.curItem = this.items[current - 1];
this.replaceHistory(current);
} }
}); });
}); });
} },
/**
bind(); * Marks progress as 100% and jumps to the next entry
}); *
$('#page-select').change(function(){ * @param {string} nextUrl - URL of the next entry
jumpTo(parseInt($('#page-select').val())); */
}); nextEntry(nextUrl) {
function showControl(idx) { this.saveProgress(this.items.length, () => {
const pageCount = $('#page-select > option').length; this.redirect(nextUrl);
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`; });
$('#progress-label').text(progressText); },
$('#page-select').val(idx); /**
UIkit.modal($('#modal-sections')).show(); * Exits the reader, and optionally sets the reading progress tp 100%
styleModal(); *
} * @param {string} exitUrl - The Exit URL
function jumpTo(page) { * @param {boolean} [markCompleted] - Whether we should mark the
var ary = window.location.pathname.split('/'); * reading progress to 100%
ary[ary.length - 1] = page; */
ary.shift(); // remove leading `/` exitReader(exitUrl, markCompleted = false) {
ary.unshift(window.location.origin); if (!markCompleted) {
window.location.replace(ary.join('/')); return this.redirect(exitUrl);
} }
function replaceHistory(url) { this.saveProgress(this.items.length, () => {
history.replaceState(null, "", url); this.redirect(exitUrl);
console.log('reading ' + url); });
} }
function redirect(url) { };
window.location.replace(url);
} }
-5
View File
File diff suppressed because one or more lines are too long
+7 -116
View File
@@ -1,124 +1,15 @@
$(() => { $(() => {
const sortItems = () => { $('#sort-select').change(() => {
const sort = $('#sort-select').find(':selected').attr('id'); const sort = $('#sort-select').find(':selected').attr('id');
const ary = sort.split('-'); const ary = sort.split('-');
const by = ary[0]; const by = ary[0];
const dir = ary[1]; const dir = ary[1];
let items = $('.item'); const url = `${location.protocol}//${location.host}${location.pathname}`;
items.remove(); const newURL = `${url}?${$.param({
sort: by,
const ctxAry = []; ascend: dir === 'up' ? 1 : 0
const keyRange = {}; })}`;
if (by === 'auto') { window.location.href = newURL;
// intelligent sorting
items.each((i, item) => {
const name = $(item).find('.uk-card-title').text();
const regex = /([^0-9\n\r\ ]*)[ ]*([0-9]*\.*[0-9]+)/g;
const numbers = {};
let match = regex.exec(name);
while (match) {
const key = match[1];
const num = parseFloat(match[2]);
numbers[key] = num;
if (!keyRange[key]) {
keyRange[key] = [num, num, 1];
}
else {
keyRange[key][2] += 1;
if (num < keyRange[key][0]) {
keyRange[key][0] = num;
}
else if (num > keyRange[key][1]) {
keyRange[key][1] = num;
}
}
match = regex.exec(name);
}
ctxAry.push({index: i, numbers: numbers});
});
console.log(keyRange);
const sortedKeys = Object.keys(keyRange).filter(k => {
return keyRange[k][2] >= items.length / 2;
});
sortedKeys.sort((a, b) => {
// sort by frequency of the key first
if (keyRange[a][2] !== keyRange[b][2]) {
return keyRange[a][2] < keyRange[b][2];
}
// then sort by range of the key
return (keyRange[a][1] - keyRange[a][0]) < (keyRange[b][1] - keyRange[b][0]);
});
console.log(sortedKeys);
ctxAry.sort((a, b) => {
for (let i = 0; i < sortedKeys.length; i++) {
const key = sortedKeys[i];
if (a.numbers[key] === undefined && b.numbers[key] === undefined)
continue;
if (a.numbers[key] === undefined)
return 1;
if (b.numbers[key] === undefined)
return -1;
if (a.numbers[key] === b.numbers[key])
continue;
return a.numbers[key] > b.numbers[key];
}
return 0;
});
const sortedItems = [];
ctxAry.forEach(ctx => {
sortedItems.push(items[ctx.index]);
});
items = sortedItems;
if (dir === 'down') {
items.reverse();
}
}
else {
items.sort((a, b) => {
var res;
if (by === 'name')
res = $(a).find('.uk-card-title').text() > $(b).find('.uk-card-title').text();
else if (by === 'date')
res = $(a).attr('data-mtime') > $(b).attr('data-mtime');
else if (by === 'progress') {
const ap = parseFloat($(a).attr('data-progress'));
const bp = parseFloat($(b).attr('data-progress'));
if (ap === bp)
// if progress is the same, we compare by name
res = $(a).find('.uk-card-title').text() > $(b).find('.uk-card-title').text();
else
res = ap > bp;
}
if (dir === 'up')
return res;
else
return !res;
});
}
var html = '';
$('#item-container').append(items);
};
$('#sort-select').change(() => {
sortItems();
}); });
if ($('option#auto-up').length > 0)
$('option#auto-up').attr('selected', '');
else
$('option#name-up').attr('selected', '');
sortItems();
}); });
-78
View File
@@ -1,78 +0,0 @@
const getTheme = () => {
var theme = localStorage.getItem('theme');
if (!theme) theme = 'light';
return theme;
};
const saveTheme = theme => {
localStorage.setItem('theme', theme);
};
const toggleTheme = () => {
const theme = getTheme();
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
saveTheme(newTheme);
};
// https://stackoverflow.com/a/28344281
const hasClass = (ele,cls) => {
return !!ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
};
const addClass = (ele,cls) => {
if (!hasClass(ele,cls)) ele.className += " "+cls;
};
const removeClass = (ele,cls) => {
if (hasClass(ele,cls)) {
var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
ele.className=ele.className.replace(reg,' ');
}
};
const addClassToClass = (targetCls, newCls) => {
const elements = document.getElementsByClassName(targetCls);
for (let i = 0; i < elements.length; i++) {
addClass(elements[i], newCls);
}
};
const removeClassFromClass = (targetCls, newCls) => {
const elements = document.getElementsByClassName(targetCls);
for (let i = 0; i < elements.length; i++) {
removeClass(elements[i], newCls);
}
};
const setTheme = themeStr => {
if (themeStr === 'dark') {
document.getElementsByTagName('html')[0].style.background = 'rgb(20, 20, 20)';
addClass(document.getElementsByTagName('body')[0], 'uk-light');
addClassToClass('uk-card', 'uk-card-secondary');
removeClassFromClass('uk-card', 'uk-card-default');
addClassToClass('ui-widget-content', 'dark');
}
else {
document.getElementsByTagName('html')[0].style.background = '';
removeClass(document.getElementsByTagName('body')[0], 'uk-light');
removeClassFromClass('uk-card', 'uk-card-secondary');
addClassToClass('uk-card', 'uk-card-default');
removeClassFromClass('ui-widget-content', 'dark');
}
};
const styleModal = () => {
const color = getTheme() === 'dark' ? '#222' : '';
$('.uk-modal-header').css('background', color);
$('.uk-modal-body').css('background', color);
$('.uk-modal-footer').css('background', color);
};
// do it before document is ready to prevent the initial flash of white
setTheme(getTheme());
document.addEventListener('DOMContentLoaded', () => {
// because this script is attached at the top of HTML, the style on uk-card
// won't be applied because the elements are not available yet. We have to
// apply the theme again for it to take effect
setTheme(getTheme());
}, false);
+311 -20
View File
@@ -1,45 +1,336 @@
$(() => {
setupAcard();
});
const setupAcard = () => {
$('.acard.is_entry').click((e) => {
if ($(e.target).hasClass('no-modal')) return;
const card = $(e.target).closest('.acard');
showModal(
$(card).attr('data-encoded-path'),
parseInt($(card).attr('data-pages')),
parseFloat($(card).attr('data-progress')),
$(card).attr('data-encoded-book-title'),
$(card).attr('data-encoded-title'),
$(card).attr('data-book-id'),
$(card).attr('data-id')
);
});
};
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) { function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
const zipPath = decodeURIComponent(encodedPath); const zipPath = decodeURIComponent(encodedPath);
const title = decodeURIComponent(encodedeTitle); const title = decodeURIComponent(encodedeTitle);
const entry = decodeURIComponent(encodedEntryTitle); const entry = decodeURIComponent(encodedEntryTitle);
$('#modal button, #modal a').each(function(){ $('#modal button, #modal a').each(function() {
$(this).removeAttr('hidden'); $(this).removeAttr('hidden');
}); });
if (percentage === 0) { if (percentage === 0) {
$('#continue-btn').attr('hidden', ''); $('#continue-btn').attr('hidden', '');
$('#unread-btn').attr('hidden', ''); $('#unread-btn').attr('hidden', '');
} } else if (percentage === 100) {
else { $('#read-btn').attr('hidden', '');
$('#continue-btn').attr('hidden', '');
} else {
$('#continue-btn').text('Continue from ' + percentage + '%'); $('#continue-btn').text('Continue from ' + percentage + '%');
} }
if (percentage === 100) {
$('#read-btn').attr('hidden', ''); $('#modal-entry-title').find('span').text(entry);
} $('#modal-entry-title').next().attr('data-id', titleID);
$('#modal-title').text(entry); $('#modal-entry-title').next().attr('data-entry-id', entryID);
$('#modal-entry-title').next().find('.title-rename-field').val(entry);
$('#path-text').text(zipPath); $('#path-text').text(zipPath);
$('#pages-text').text(pages + ' pages'); $('#pages-text').text(pages + ' pages');
$('#beginning-btn').attr('href', '/reader/' + titleID + '/' + entryID + '/1'); $('#beginning-btn').attr('href', `${base_url}reader/${titleID}/${entryID}/1`);
$('#continue-btn').attr('href', '/reader/' + titleID + '/' + entryID); $('#continue-btn').attr('href', `${base_url}reader/${titleID}/${entryID}`);
$('#read-btn').click(function(){ $('#read-btn').click(function() {
updateProgress(titleID, entryID, pages); updateProgress(titleID, entryID, pages);
}); });
$('#unread-btn').click(function(){ $('#unread-btn').click(function() {
updateProgress(titleID, entryID, 0); updateProgress(titleID, entryID, 0);
}); });
$('#modal-edit-btn').attr('onclick', `edit("${entryID}")`);
$('#modal-download-btn').attr('href', `${base_url}api/download/${titleID}/${entryID}`);
UIkit.modal($('#modal')).show(); UIkit.modal($('#modal')).show();
styleModal();
} }
function updateProgress(titleID, entryID, page) {
$.post('/api/progress/' + titleID + '/' + entryID + '/' + page, function(data) { const updateProgress = (tid, eid, page) => {
if (data.success) { let url = `${base_url}api/progress/${tid}/${page}`
const query = $.param({
eid: eid
});
if (eid)
url += `?${query}`;
$.ajax({
method: 'PUT',
url: url,
dataType: 'json'
})
.done(data => {
if (data.success) {
location.reload();
} else {
error = data.error;
alert('danger', error);
}
})
.fail((jqXHR, status) => {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
};
const renameSubmit = (name, eid) => {
const upload = $('.upload-field');
const titleId = upload.attr('data-title-id');
console.log(name);
if (name.length === 0) {
alert('danger', 'The display name should not be empty');
return;
}
const query = $.param({
eid: eid
});
let url = `${base_url}api/admin/display_name/${titleId}/${name}`;
if (eid)
url += `?${query}`;
$.ajax({
type: 'PUT',
url: url,
contentType: "application/json",
dataType: 'json'
})
.done(data => {
if (data.error) {
alert('danger', `Failed to update display name. Error: ${data.error}`);
return;
}
location.reload(); location.reload();
} })
else { .fail((jqXHR, status) => {
error = data.error; alert('danger', `Failed to update display name. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
alert('danger', error); });
};
const edit = (eid) => {
const cover = $('#edit-modal #cover');
let url = cover.attr('data-title-cover');
let displayName = $('h2.uk-title > span').text();
if (eid) {
const item = $(`#${eid}`);
url = item.find('img').attr('data-src');
displayName = item.find('.uk-card-title').attr('data-title');
$('#title-progress-control').attr('hidden', '');
} else {
$('#title-progress-control').removeAttr('hidden');
}
cover.attr('data-src', url);
const displayNameField = $('#display-name-field');
displayNameField.attr('value', displayName);
console.log(displayNameField);
displayNameField.keyup(event => {
if (event.keyCode === 13) {
renameSubmit(displayNameField.val(), eid);
} }
}); });
} displayNameField.siblings('a.uk-form-icon').click(() => {
renameSubmit(displayNameField.val(), eid);
});
setupUpload(eid);
UIkit.modal($('#edit-modal')).show();
};
const setupUpload = (eid) => {
const upload = $('.upload-field');
const bar = $('#upload-progress').get(0);
const titleId = upload.attr('data-title-id');
const queryObj = {
tid: titleId
};
if (eid)
queryObj['eid'] = eid;
const query = $.param(queryObj);
const url = `${base_url}api/admin/upload/cover?${query}`;
console.log(url);
UIkit.upload('.upload-field', {
url: url,
name: 'file',
error: (e) => {
alert('danger', `Failed to upload cover image: ${e.toString()}`);
},
loadStart: (e) => {
$(bar).removeAttr('hidden');
bar.max = e.total;
bar.value = e.loaded;
},
progress: (e) => {
bar.max = e.total;
bar.value = e.loaded;
},
loadEnd: (e) => {
bar.max = e.total;
bar.value = e.loaded;
},
completeAll: () => {
$(bar).attr('hidden', '');
location.reload();
}
});
};
const deselectAll = () => {
$('.item .uk-card').each((i, e) => {
const data = e.__x.$data;
data['selected'] = false;
});
$('#select-bar')[0].__x.$data['count'] = 0;
};
const selectAll = () => {
let count = 0;
$('.item .uk-card').each((i, e) => {
const data = e.__x.$data;
if (!data['disabled']) {
data['selected'] = true;
count++;
}
});
$('#select-bar')[0].__x.$data['count'] = count;
};
const selectedIDs = () => {
const ary = [];
$('.item .uk-card').each((i, e) => {
const data = e.__x.$data;
if (!data['disabled'] && data['selected']) {
const item = $(e).closest('.item');
ary.push($(item).attr('id'));
}
});
return ary;
};
const bulkProgress = (action, el) => {
const tid = $(el).attr('data-id');
const ids = selectedIDs();
const url = `${base_url}api/bulk_progress/${action}/${tid}`;
$.ajax({
type: 'PUT',
url: url,
contentType: "application/json",
dataType: 'json',
data: JSON.stringify({
ids: ids
})
})
.done(data => {
if (data.error) {
alert('danger', `Failed to mark entries as ${action}. Error: ${data.error}`);
return;
}
location.reload();
})
.fail((jqXHR, status) => {
alert('danger', `Failed to mark entries as ${action}. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
})
.always(() => {
deselectAll();
});
};
const tagsComponent = () => {
return {
isAdmin: false,
tags: [],
tid: $('.upload-field').attr('data-title-id'),
loading: true,
load(admin) {
this.isAdmin = admin;
$('.tag-select').select2({
tags: true,
placeholder: this.isAdmin ? 'Tag the title' : 'No tags found',
disabled: !this.isAdmin,
templateSelection(state) {
const a = document.createElement('a');
a.setAttribute('href', `${base_url}tags/${encodeURIComponent(state.text)}`);
a.setAttribute('class', 'uk-link-reset');
a.onclick = event => {
event.stopPropagation();
};
a.innerText = state.text;
return a;
}
});
this.request(`${base_url}api/tags`, 'GET', (data) => {
const allTags = data.tags;
const url = `${base_url}api/tags/${this.tid}`;
this.request(url, 'GET', data => {
this.tags = data.tags;
allTags.forEach(t => {
const op = new Option(t, t, false, this.tags.indexOf(t) >= 0);
$('.tag-select').append(op);
});
$('.tag-select').on('select2:select', e => {
this.onAdd(e);
});
$('.tag-select').on('select2:unselect', e => {
this.onDelete(e);
});
$('.tag-select').on('change', () => {
this.onChange();
});
$('.tag-select').trigger('change');
this.loading = false;
});
});
},
onChange() {
this.tags = $('.tag-select').select2('data').map(o => o.text);
},
onAdd(event) {
const tag = event.params.data.text;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`;
this.request(url, 'PUT');
},
onDelete(event) {
const tag = event.params.data.text;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`;
this.request(url, 'DELETE');
},
request(url, method, cb) {
$.ajax({
url: url,
method: method,
dataType: 'json'
})
.done(data => {
if (data.success) {
if (cb) cb(data);
} else {
alert('danger', data.error);
}
})
.fail((jqXHR, status) => {
alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
}
};
};
+1 -1
View File
@@ -1,5 +1,5 @@
$(() => { $(() => {
var target = '/admin/user/edit'; var target = base_url + 'admin/user/edit';
if (username) target += username; if (username) target += username;
$('form').attr('action', target); $('form').attr('action', target);
if (error) alert('danger', error); if (error) alert('danger', error);
+16 -11
View File
@@ -1,11 +1,16 @@
function remove(username) { const remove = (username) => {
$.post('/api/admin/user/delete/' + username, function(data) { $.ajax({
if (data.success) { url: `${base_url}api/admin/user/delete/${username}`,
location.reload(); type: 'DELETE',
} dataType: 'json'
else { })
error = data.error; .done(data => {
alert('danger', error); if (data.success)
} location.reload();
}); else
} alert('danger', data.error);
})
.fail((jqXHR, status) => {
alert('danger', `Failed to delete the user. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
});
};
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+69 -21
View File
@@ -1,30 +1,78 @@
version: 1.0 version: 2.0
shards: shards:
db: ameba:
github: crystal-lang/crystal-db git: https://github.com/crystal-ameba/ameba.git
version: 0.8.0 version: 0.12.1
exception_page: archive:
github: crystal-loot/exception_page git: https://github.com/hkalexling/archive.cr.git
version: 0.1.2
kemal:
github: kemalcr/kemal
version: 0.26.1
kemal-basic-auth:
github: kemalcr/kemal-basic-auth
version: 0.2.0
kilt:
github: jeromegn/kilt
version: 0.4.0 version: 0.4.0
baked_file_system:
git: https://github.com/schovi/baked_file_system.git
version: 0.9.8+git.commit.fb3091b546797fbec3c25dc0e1e2cff60bb9033b
clim:
git: https://github.com/at-grandpa/clim.git
version: 0.12.0
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.9.0
duktape:
git: https://github.com/jessedoyle/duktape.cr.git
version: 0.20.0
exception_page:
git: https://github.com/crystal-loot/exception_page.git
version: 0.1.4
http_proxy:
git: https://github.com/mamantoha/http_proxy.git
version: 0.7.1
image_size:
git: https://github.com/hkalexling/image_size.cr.git
version: 0.4.0
kemal:
git: https://github.com/kemalcr/kemal.git
version: 0.27.0
kemal-session:
git: https://github.com/kemalcr/kemal-session.git
version: 0.12.1
kilt:
git: https://github.com/jeromegn/kilt.git
version: 0.4.0
koa:
git: https://github.com/hkalexling/koa.git
version: 0.5.0
mg:
git: https://github.com/hkalexling/mg.git
version: 0.2.0+git.commit.171c46489d991a8353818e00fc6a3c4e0809ded9
myhtml:
git: https://github.com/kostya/myhtml.git
version: 1.5.1
open_api:
git: https://github.com/jreinert/open_api.cr.git
version: 1.2.1+git.commit.95e4df2ca10b1fe88b8b35c62a18b06a10267b6c
radix: radix:
github: luislavena/radix git: https://github.com/luislavena/radix.git
version: 0.3.9 version: 0.3.9
sqlite3: sqlite3:
github: crystal-lang/crystal-sqlite3 git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.15.0 version: 0.16.0
tallboy:
git: https://github.com/epoch/tallboy.git
version: 0.9.3
+26 -2
View File
@@ -1,5 +1,5 @@
name: mango name: mango
version: 0.2.3 version: 0.19.1
authors: authors:
- Alex Ling <hkalexling@gmail.com> - Alex Ling <hkalexling@gmail.com>
@@ -8,14 +8,38 @@ targets:
mango: mango:
main: src/mango.cr main: src/mango.cr
crystal: 0.32.1 crystal: 0.35.1
license: MIT license: MIT
dependencies: dependencies:
kemal: kemal:
github: kemalcr/kemal github: kemalcr/kemal
kemal-session:
github: kemalcr/kemal-session
sqlite3: sqlite3:
github: crystal-lang/crystal-sqlite3 github: crystal-lang/crystal-sqlite3
baked_file_system: baked_file_system:
github: schovi/baked_file_system github: schovi/baked_file_system
version: 0.9.8+git.commit.fb3091b546797fbec3c25dc0e1e2cff60bb9033b
archive:
github: hkalexling/archive.cr
ameba:
github: crystal-ameba/ameba
clim:
github: at-grandpa/clim
duktape:
github: jessedoyle/duktape.cr
version: ~> 0.20.0
myhtml:
github: kostya/myhtml
http_proxy:
github: mamantoha/http_proxy
image_size:
github: hkalexling/image_size.cr
koa:
github: hkalexling/koa
tallboy:
github: epoch/tallboy
mg:
github: hkalexling/mg
+9 -9
View File
@@ -1,14 +1,14 @@
require "./spec_helper" require "./spec_helper"
describe Config do describe Config do
it "creates config if it does not exist" do it "creates config if it does not exist" do
with_default_config do |config, logger, path| with_default_config do |_, path|
File.exists?(path).should be_true File.exists?(path).should be_true
end end
end end
it "correctly loads config" do it "correctly loads config" do
config = Config.load "spec/asset/test-config.yml" config = Config.load "spec/asset/test-config.yml"
config.port.should eq 3000 config.port.should eq 3000
end end
end end
-105
View File
@@ -1,105 +0,0 @@
require "./spec_helper"
include MangaDex
describe Queue do
it "creates DB at given path" do
with_queue do |queue, path|
File.exists?(path).should be_true
end
end
it "pops nil when empty" do
with_queue do |queue|
queue.pop.should be_nil
end
end
it "inserts multiple jobs" do
with_queue do |queue|
j1 = Job.new "1", "1", "title", "manga_title", JobStatus::Error,
Time.utc
j2 = Job.new "2", "2", "title", "manga_title", JobStatus::Completed,
Time.utc
j3 = Job.new "3", "3", "title", "manga_title", JobStatus::Pending,
Time.utc
j4 = Job.new "4", "4", "title", "manga_title",
JobStatus::Downloading, Time.utc
count = queue.push [j1, j2, j3, j4]
count.should eq 4
end
end
it "pops pending job" do
with_queue do |queue|
job = queue.pop
job.should_not be_nil
job.not_nil!.id.should eq "3"
end
end
it "correctly counts jobs" do
with_queue do |queue|
queue.count.should eq 4
end
end
it "deletes job" do
with_queue do |queue|
queue.delete "4"
queue.count.should eq 3
end
end
it "sets status" do
with_queue do |queue|
job = queue.pop.not_nil!
queue.set_status JobStatus::Downloading, job
job = queue.pop
job.should_not be_nil
job.not_nil!.status.should eq JobStatus::Downloading
end
end
it "sets number of pages" do
with_queue do |queue|
job = queue.pop.not_nil!
queue.set_pages 100, job
job = queue.pop
job.should_not be_nil
job.not_nil!.pages.should eq 100
end
end
it "adds fail/success counts" do
with_queue do |queue|
job = queue.pop.not_nil!
queue.add_success job
queue.add_success job
queue.add_fail job
job = queue.pop
job.should_not be_nil
job.not_nil!.success_count.should eq 2
job.not_nil!.fail_count.should eq 1
end
end
it "appends status message" do
with_queue do |queue|
job = queue.pop.not_nil!
queue.add_message "hello", job
queue.add_message "world", job
job = queue.pop
job.should_not be_nil
job.not_nil!.status_message.should eq "\nhello\nworld"
end
end
it "cleans up" do
with_queue do
true
end
State.reset
end
end
+76
View File
@@ -0,0 +1,76 @@
require "./spec_helper"
require "../src/rename"
include Rename
describe Rule do
it "raises on nested brackets" do
expect_raises Exception do
Rule.new "[[]]"
end
expect_raises Exception do
Rule.new "{{}}"
end
end
it "raises on unclosed brackets" do
expect_raises Exception do
Rule.new "["
end
expect_raises Exception do
Rule.new "{"
end
expect_raises Exception do
Rule.new "[{]}"
end
end
it "raises when closing unopened brackets" do
expect_raises Exception do
Rule.new "]"
end
expect_raises Exception do
Rule.new "[}"
end
end
it "handles `|` in patterns" do
rule = Rule.new "{a|b|c}"
rule.render({"b" => "b"}).should eq "b"
rule.render({"a" => "a", "b" => "b"}).should eq "a"
end
it "raises on escaped characters" do
expect_raises Exception do
Rule.new "hello/world"
end
end
it "handles spaces in patterns" do
rule = Rule.new "{ a }"
rule.render({"a" => "a"}).should eq "a"
end
it "strips leading and tailing spaces" do
rule = Rule.new " hello "
rule.render({"a" => "a"}).should eq "hello"
end
it "renders a few examples correctly" do
rule = Rule.new "[Ch. {chapter }] {title | id} testing"
rule.render({"id" => "ID"}).should eq "ID testing"
rule.render({"chapter" => "CH", "id" => "ID"})
.should eq "Ch. CH ID testing"
rule.render({} of String => String).should eq "testing"
end
it "escapes illegal characters" do
rule = Rule.new "{a}"
rule.render({"a" => "/?<>:*|\"^"}).should eq "_________"
end
it "strips trailing spaces and dots" do
rule = Rule.new "hello. world. .."
rule.render({} of String => String).should eq "hello. world"
end
end
+38 -47
View File
@@ -1,65 +1,56 @@
require "spec" require "spec"
require "../src/context" require "../src/queue"
require "../src/server" require "../src/server"
require "../src/config"
require "../src/main_fiber"
class State class State
@@hash = {} of String => String @@hash = {} of String => String
def self.get(key) def self.get(key)
@@hash[key]? @@hash[key]?
end end
def self.get!(key) def self.get!(key)
@@hash[key] @@hash[key]
end end
def self.set(key, value) def self.set(key, value)
return if value.nil? return if value.nil?
@@hash[key] = value @@hash[key] = value
end end
def self.reset def self.reset
@@hash.clear @@hash.clear
end end
end end
def get_tempfile(name) def get_tempfile(name)
path = State.get name path = State.get name
if path.nil? || !File.exists? path if path.nil? || !File.exists? path
file = File.tempfile name file = File.tempfile name
State.set name, file.path State.set name, file.path
return file file
else else
return File.new path File.new path
end end
end end
def with_default_config def with_default_config
temp_config = get_tempfile "mango-test-config" temp_config = get_tempfile "mango-test-config"
config = Config.load temp_config.path config = Config.load temp_config.path
logger = MLogger.new config config.set_current
yield config, logger, temp_config.path yield config, temp_config.path
temp_config.delete temp_config.delete
end end
def with_storage def with_storage
with_default_config do |config, logger| with_default_config do
temp_db = get_tempfile "mango-test-db" temp_db = get_tempfile "mango-test-db"
storage = Storage.new temp_db.path, logger storage = Storage.new temp_db.path, false
clear = yield storage, temp_db.path clear = yield storage, temp_db.path
if clear == true if clear == true
temp_db.delete temp_db.delete
end end
end end
end
def with_queue
with_default_config do |config, logger|
temp_queue_db = get_tempfile "mango-test-queue-db"
queue = MangaDex::Queue.new temp_queue_db.path, logger
clear = yield queue, temp_queue_db.path
if clear == true
temp_queue_db.delete
end
end
end end
+77 -77
View File
@@ -1,91 +1,91 @@
require "./spec_helper" require "./spec_helper"
describe Storage do describe Storage do
it "creates DB at given path" do it "creates DB at given path" do
with_storage do |storage, path| with_storage do |_, path|
File.exists?(path).should be_true File.exists?(path).should be_true
end end
end end
it "deletes user" do it "deletes user" do
with_storage do |storage| with_storage do |storage|
storage.delete_user "admin" storage.delete_user "admin"
end end
end end
it "creates new user" do it "creates new user" do
with_storage do |storage| with_storage do |storage|
storage.new_user "user", "123456", false storage.new_user "user", "123456", false
storage.new_user "admin", "123456", true storage.new_user "admin", "123456", true
end end
end end
it "verifies username/password combination" do it "verifies username/password combination" do
with_storage do |storage| with_storage do |storage|
user_token = storage.verify_user "user", "123456" user_token = storage.verify_user "user", "123456"
admin_token = storage.verify_user "admin", "123456" admin_token = storage.verify_user "admin", "123456"
user_token.should_not be_nil user_token.should_not be_nil
admin_token.should_not be_nil admin_token.should_not be_nil
State.set "user_token", user_token State.set "user_token", user_token
State.set "admin_token", admin_token State.set "admin_token", admin_token
end end
end end
it "rejects duplicate username" do it "rejects duplicate username" do
with_storage do |storage| with_storage do |storage|
expect_raises SQLite3::Exception, expect_raises SQLite3::Exception,
"UNIQUE constraint failed: users.username" do "UNIQUE constraint failed: users.username" do
storage.new_user "admin", "123456", true storage.new_user "admin", "123456", true
end end
end end
end end
it "verifies token" do it "verifies token" do
with_storage do |storage| with_storage do |storage|
user_token = State.get! "user_token" user_token = State.get! "user_token"
user = storage.verify_token user_token user = storage.verify_token user_token
user.should eq "user" user.should eq "user"
end end
end end
it "verfies admin token" do it "verfies admin token" do
with_storage do |storage| with_storage do |storage|
admin_token = State.get! "admin_token" admin_token = State.get! "admin_token"
storage.verify_admin(admin_token).should be_true storage.verify_admin(admin_token).should be_true
end end
end end
it "rejects non-admin token" do it "rejects non-admin token" do
with_storage do |storage| with_storage do |storage|
user_token = State.get! "user_token" user_token = State.get! "user_token"
storage.verify_admin(user_token).should be_false storage.verify_admin(user_token).should be_false
end end
end end
it "updates user" do it "updates user" do
with_storage do |storage| with_storage do |storage|
storage.update_user "admin", "admin", "654321", true storage.update_user "admin", "admin", "654321", true
token = storage.verify_user "admin", "654321" token = storage.verify_user "admin", "654321"
admin_token = State.get! "admin_token" admin_token = State.get! "admin_token"
token.should eq admin_token token.should eq admin_token
end end
end end
it "logs user out" do it "logs user out" do
with_storage do |storage| with_storage do |storage|
user_token = State.get! "user_token" user_token = State.get! "user_token"
admin_token = State.get! "admin_token" admin_token = State.get! "admin_token"
storage.logout user_token storage.logout user_token
storage.logout admin_token storage.logout admin_token
storage.verify_token(user_token).should be_nil storage.verify_token(user_token).should be_nil
storage.verify_token(admin_token).should be_nil storage.verify_token(admin_token).should be_nil
end end
end end
it "cleans up" do it "cleans up" do
with_storage do with_storage do
true true
end end
State.reset State.reset
end end
end end
+40 -30
View File
@@ -1,36 +1,46 @@
require "./spec_helper" require "./spec_helper"
describe "compare_alphanumerically" do describe "compare_numerically" do
it "sorts filenames with leading zeros correctly" do it "sorts filenames with leading zeros correctly" do
ary = ["010.jpg", "001.jpg", "002.png"] ary = ["010.jpg", "001.jpg", "002.png"]
ary.sort! {|a, b| ary.sort! { |a, b|
compare_alphanumerically a, b compare_numerically a, b
} }
ary.should eq ["001.jpg", "002.png", "010.jpg"] ary.should eq ["001.jpg", "002.png", "010.jpg"]
end end
it "sorts filenames without leading zeros correctly" do it "sorts filenames without leading zeros correctly" do
ary = ["10.jpg", "1.jpg", "0.png", "0100.jpg"] ary = ["10.jpg", "1.jpg", "0.png", "0100.jpg"]
ary.sort! {|a, b| ary.sort! { |a, b|
compare_alphanumerically a, b compare_numerically a, b
} }
ary.should eq ["0.png", "1.jpg", "10.jpg", "0100.jpg"] ary.should eq ["0.png", "1.jpg", "10.jpg", "0100.jpg"]
end end
# https://ux.stackexchange.com/a/95441 # https://ux.stackexchange.com/a/95441
it "sorts like the stack exchange post" do it "sorts like the stack exchange post" do
ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2", ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2",
"text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"] "text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"]
ary.reverse.sort {|a, b| ary.reverse.sort { |a, b|
compare_alphanumerically a, b compare_numerically a, b
}.should eq ary }.should eq ary
end end
# https://github.com/hkalexling/Mango/issues/22 # https://github.com/hkalexling/Mango/issues/22
it "handles numbers larger than Int32" do it "handles numbers larger than Int32" do
ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"] ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"]
ary.reverse.sort {|a, b| ary.reverse.sort { |a, b|
compare_alphanumerically a, b compare_numerically a, b
}.should eq ary }.should eq ary
end end
end
describe "chapter_sort" do
it "sorts correctly" do
ary = ["Vol.1 Ch.01", "Vol.1 Ch.02", "Vol.2 Ch. 2.5", "Ch. 3", "Ch.04"]
sorter = ChapterSorter.new ary
ary.reverse.sort do |a, b|
sorter.compare a, b
end.should eq ary
end
end end
+59
View File
@@ -0,0 +1,59 @@
require "compress/zip"
require "archive"
# 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 = Compress::Zip::File.new filename
else
@archive_file = Archive::File.new filename
end
end
def self.open(filename : String, &)
s = self.new filename
yield s
s.close
end
def 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 Compress::Zip::File::Entry | Archive::Entry
@archive_file.entries.map do |e|
if (e.is_a? Compress::Zip::File::Entry && e.file?) ||
(e.is_a? Archive::Entry && e.info.file?)
ary.push e
end
end
ary
end
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
bytes_read = io.read_fully? slice
data = slice if bytes_read
end
data
else
e.read
end
end
def check
if @archive_file.is_a? Archive::File
@archive_file.as(Archive::File).check
end
end
end
-26
View File
@@ -1,26 +0,0 @@
require "kemal"
require "./storage"
require "./util"
class AuthHandler < Kemal::Handler
def initialize(@storage : Storage)
end
def call(env)
return call_next(env) \
if request_path_startswith env, ["/login", "/logout"]
cookie = env.request.cookies.find { |c| c.name == "token" }
if cookie.nil? || ! @storage.verify_token cookie.value
return env.redirect "/login"
end
if request_path_startswith env, ["/admin", "/api/admin", "/download"]
unless @storage.verify_admin cookie.value
env.response.status_code = 403
end
end
call_next env
end
end
+88 -52
View File
@@ -1,60 +1,96 @@
require "yaml" require "yaml"
class Config class Config
include YAML::Serializable include YAML::Serializable
property port : Int32 = 9000 @[YAML::Field(ignore: true)]
property library_path : String = \ property path : String = ""
File.expand_path "~/mango/library", home: true property port : Int32 = 9000
property db_path : String = \ property base_url : String = "/"
File.expand_path "~/mango/mango.db", home: true property session_secret : String = "mango-session-secret"
@[YAML::Field(key: "scan_interval_minutes")] property library_path : String = File.expand_path "~/mango/library",
property scan_interval : Int32 = 5 home: true
property log_level : String = "info" property db_path : String = File.expand_path "~/mango/mango.db", home: true
property mangadex = Hash(String, String|Int32).new property scan_interval_minutes : Int32 = 5
property thumbnail_generation_interval_hours : Int32 = 24
property db_optimization_interval_hours : Int32 = 24
property log_level : String = "info"
property upload_path : String = File.expand_path "~/mango/uploads",
home: true
property plugin_path : String = File.expand_path "~/mango/plugins",
home: true
property download_timeout_seconds : Int32 = 30
property page_margin : Int32 = 30
property disable_login = false
property default_username = ""
property mangadex = Hash(String, String | Int32).new
@[YAML::Field(ignore: true)] @[YAML::Field(ignore: true)]
@mangadex_defaults = { @mangadex_defaults = {
"base_url" => "https://mangadex.org", "base_url" => "https://mangadex.org",
"api_url" => "https://mangadex.org/api", "api_url" => "https://mangadex.org/api",
"download_wait_seconds" => 5, "download_wait_seconds" => 5,
"download_retries" => 4, "download_retries" => 4,
"download_queue_db_path" => File.expand_path "~/mango/queue.db", "download_queue_db_path" => File.expand_path("~/mango/queue.db",
home: true home: true),
} "chapter_rename_rule" => "[Vol.{volume} ][Ch.{chapter} ]{title|id}",
"manga_rename_rule" => "{title}",
}
def self.load(path : String?) @@singlet : Config?
path = "~/.config/mango/config.yml" if path.nil?
cfg_path = File.expand_path path, home: true
if File.exists? cfg_path
config = self.from_yaml File.read cfg_path
config.fill_defaults
return config
end
puts "The config file #{cfg_path} does not exist." \
" Do you want mango to dump the default config there? [Y/n]"
input = gets
if input && input.downcase == "n"
abort "Aborting..."
end
default = self.allocate
default.fill_defaults
cfg_dir = File.dirname cfg_path
unless Dir.exists? cfg_dir
Dir.mkdir_p cfg_dir
end
File.write cfg_path, default.to_yaml
puts "The config file has been created at #{cfg_path}."
default
end
def fill_defaults def self.current
{% for hash_name in ["mangadex"] %} @@singlet.not_nil!
@{{hash_name.id}}_defaults.map do |k, v| end
if @{{hash_name.id}}[k]?.nil?
@{{hash_name.id}}[k] = v def set_current
end @@singlet = self
end end
{% end %}
end def self.load(path : String?)
path = "~/.config/mango/config.yml" if path.nil?
cfg_path = File.expand_path path, home: true
if File.exists? cfg_path
config = self.from_yaml File.read cfg_path
config.preprocess
config.path = path
config.fill_defaults
return config
end
puts "The config file #{cfg_path} does not exist. " \
"Dumping the default config there."
default = self.allocate
default.path = path
default.fill_defaults
cfg_dir = File.dirname cfg_path
unless Dir.exists? cfg_dir
Dir.mkdir_p cfg_dir
end
File.write cfg_path, default.to_yaml
puts "The config file has been created at #{cfg_path}."
default
end
def fill_defaults
{% for hash_name in ["mangadex"] %}
@{{hash_name.id}}_defaults.map do |k, v|
if @{{hash_name.id}}[k]?.nil?
@{{hash_name.id}}[k] = v
end
end
{% end %}
end
def preprocess
unless base_url.starts_with? "/"
raise "base url (#{base_url}) should start with `/`"
end
unless base_url.ends_with? "/"
@base_url += "/"
end
if disable_login && default_username.empty?
raise "Login is disabled, but default username is not set. " \
"Please set a default username"
end
end
end end
-21
View File
@@ -1,21 +0,0 @@
require "./config"
require "./library"
require "./storage"
require "./logger"
class Context
property config : Config
property library : Library
property storage : Storage
property logger : MLogger
property queue : MangaDex::Queue
def initialize(@config, @logger, @library, @storage, @queue)
end
{% for lvl in LEVELS %}
def {{lvl.id}}(msg)
@logger.{{lvl.id}} msg
end