mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 10:55:30 -04:00
Merge branch 'dev' into feature/signature
This commit is contained in:
commit
7f76322377
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ mango
|
|||||||
public/css/uikit.css
|
public/css/uikit.css
|
||||||
public/img/*.svg
|
public/img/*.svg
|
||||||
public/js/*.min.js
|
public/js/*.min.js
|
||||||
|
public/css/*.css
|
||||||
|
@ -52,7 +52,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango - Manga Server and Web Reader. Version 0.18.3
|
Mango - Manga Server and Web Reader. Version 0.19.0
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
85
migration/foreign_keys.6.cr
Normal file
85
migration/foreign_keys.6.cr
Normal 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
migration/ids.2.cr
Normal file
19
migration/ids.2.cr
Normal 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
migration/tags.4.cr
Normal file
19
migration/tags.4.cr
Normal 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
migration/thumbnails.3.cr
Normal file
20
migration/thumbnails.3.cr
Normal 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
migration/titles.5.cr
Normal file
56
migration/titles.5.cr
Normal 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
migration/users.1.cr
Normal file
20
migration/users.1.cr
Normal 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
|
@ -1,146 +0,0 @@
|
|||||||
.uk-alert-close {
|
|
||||||
color: black !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-card-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-card-media-top {
|
|
||||||
width: 100%;
|
|
||||||
height: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
.uk-card-media-top {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-card-media-top>img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-card-title {
|
|
||||||
max-height: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acard:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-bg {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.break-word {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-logo>img {
|
|
||||||
height: 90px;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selectable .ui-selecting {
|
|
||||||
background: #EEE6B9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selectable .ui-selected {
|
|
||||||
background: #F4E487;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-light #selectable .ui-selecting {
|
|
||||||
background: #5E5731;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-light #selectable .ui-selected {
|
|
||||||
background: #9D9252;
|
|
||||||
}
|
|
||||||
|
|
||||||
td>.uk-dropdown {
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
||||||
#edit-modal .uk-grid>div {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#edit-modal #cover {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
#edit-modal #cover-upload {
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#edit-modal .uk-modal-body .uk-inline {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item .uk-card-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grayscale {
|
|
||||||
filter: grayscale(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-light .uk-navbar-dropdown,
|
|
||||||
.uk-light .uk-modal-header,
|
|
||||||
.uk-light .uk-modal-body,
|
|
||||||
.uk-light .uk-modal-footer {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-light .uk-dropdown {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-light .uk-navbar-dropdown,
|
|
||||||
.uk-light .uk-dropdown {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uk-light .uk-nav-header,
|
|
||||||
.uk-light .uk-description-list>dt {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
[x-cloak] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#select-bar-controls a {
|
|
||||||
transform: scale(1.5, 1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#select-bar-controls a:hover {
|
|
||||||
color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main-section {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#totop-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 100vh;
|
|
||||||
right: 2em;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#totop-wrapper a {
|
|
||||||
position: fixed;
|
|
||||||
position: sticky;
|
|
||||||
top: calc(100vh - 5em);
|
|
||||||
}
|
|
124
public/css/mango.less
Normal file
124
public/css/mango.less
Normal 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
public/css/tags.less
Normal file
58
public/css/tags.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -255,48 +255,65 @@ const bulkProgress = (action, el) => {
|
|||||||
|
|
||||||
const tagsComponent = () => {
|
const tagsComponent = () => {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
tags: [],
|
tags: [],
|
||||||
newTag: '',
|
|
||||||
inputShown: false,
|
|
||||||
tid: $('.upload-field').attr('data-title-id'),
|
tid: $('.upload-field').attr('data-title-id'),
|
||||||
|
loading: true,
|
||||||
|
|
||||||
load(admin) {
|
load(admin) {
|
||||||
this.isAdmin = admin;
|
this.isAdmin = admin;
|
||||||
const url = `${base_url}api/tags/${this.tid}`;
|
|
||||||
this.request(url, 'GET', (data) => {
|
$('.tag-select').select2({
|
||||||
this.tags = data.tags;
|
tags: true,
|
||||||
this.loading = false;
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
add() {
|
this.request(`${base_url}api/tags`, 'GET', (data) => {
|
||||||
const tag = this.newTag.trim();
|
const allTags = data.tags;
|
||||||
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`;
|
const url = `${base_url}api/tags/${this.tid}`;
|
||||||
this.request(url, 'PUT', () => {
|
this.request(url, 'GET', data => {
|
||||||
this.tags.push(tag);
|
this.tags = data.tags;
|
||||||
this.newTag = '';
|
allTags.forEach(t => {
|
||||||
});
|
const op = new Option(t, t, false, this.tags.indexOf(t) >= 0);
|
||||||
},
|
$('.tag-select').append(op);
|
||||||
keydown(event) {
|
});
|
||||||
if (event.key === 'Enter')
|
$('.tag-select').on('select2:select', e => {
|
||||||
this.add()
|
this.onAdd(e);
|
||||||
},
|
});
|
||||||
rm(event) {
|
$('.tag-select').on('select2:unselect', e => {
|
||||||
const tag = event.currentTarget.id.split('-')[0];
|
this.onDelete(e);
|
||||||
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`;
|
});
|
||||||
this.request(url, 'DELETE', () => {
|
$('.tag-select').on('change', () => {
|
||||||
const idx = this.tags.indexOf(tag);
|
this.onChange();
|
||||||
if (idx < 0) return;
|
});
|
||||||
this.tags.splice(idx, 1);
|
$('.tag-select').trigger('change');
|
||||||
});
|
this.loading = false;
|
||||||
},
|
|
||||||
toggleInput(nextTick) {
|
|
||||||
this.inputShown = !this.inputShown;
|
|
||||||
if (this.inputShown) {
|
|
||||||
nextTick(() => {
|
|
||||||
$('#tag-input').get(0).focus();
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
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) {
|
request(url, method, cb) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -305,9 +322,9 @@ const tagsComponent = () => {
|
|||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
})
|
})
|
||||||
.done(data => {
|
.done(data => {
|
||||||
if (data.success)
|
if (data.success) {
|
||||||
cb(data);
|
if (cb) cb(data);
|
||||||
else {
|
} else {
|
||||||
alert('danger', data.error);
|
alert('danger', data.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -52,6 +52,10 @@ shards:
|
|||||||
git: https://github.com/hkalexling/koa.git
|
git: https://github.com/hkalexling/koa.git
|
||||||
version: 0.5.0
|
version: 0.5.0
|
||||||
|
|
||||||
|
mg:
|
||||||
|
git: https://github.com/hkalexling/mg.git
|
||||||
|
version: 0.2.0+git.commit.171c46489d991a8353818e00fc6a3c4e0809ded9
|
||||||
|
|
||||||
myhtml:
|
myhtml:
|
||||||
git: https://github.com/kostya/myhtml.git
|
git: https://github.com/kostya/myhtml.git
|
||||||
version: 1.5.1
|
version: 1.5.1
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.18.3
|
version: 0.19.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
@ -41,3 +41,5 @@ dependencies:
|
|||||||
github: hkalexling/koa
|
github: hkalexling/koa
|
||||||
tallboy:
|
tallboy:
|
||||||
github: epoch/tallboy
|
github: epoch/tallboy
|
||||||
|
mg:
|
||||||
|
github: hkalexling/mg
|
||||||
|
@ -82,7 +82,12 @@ class AuthHandler < Kemal::Handler
|
|||||||
if env.session.string? "token"
|
if env.session.string? "token"
|
||||||
should_reject = !validate_token_admin(env)
|
should_reject = !validate_token_admin(env)
|
||||||
end
|
end
|
||||||
env.response.status_code = 403 if should_reject
|
if should_reject
|
||||||
|
env.response.status_code = 403
|
||||||
|
send_error_page "HTTP 403: You are not authorized to visit " \
|
||||||
|
"#{env.request.path}"
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
call_next env
|
call_next env
|
||||||
|
@ -6,26 +6,14 @@ class Logger
|
|||||||
SEVERITY_IDS = [0, 4, 5, 2, 3]
|
SEVERITY_IDS = [0, 4, 5, 2, 3]
|
||||||
COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta]
|
COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta]
|
||||||
|
|
||||||
|
getter raw_log = Log.for ""
|
||||||
|
|
||||||
@@severity : Log::Severity = :info
|
@@severity : Log::Severity = :info
|
||||||
|
|
||||||
use_default
|
use_default
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
level = Config.current.log_level
|
@@severity = Logger.get_severity
|
||||||
{% begin %}
|
|
||||||
case level.downcase
|
|
||||||
when "off"
|
|
||||||
@@severity = :none
|
|
||||||
{% for lvl, i in LEVELS %}
|
|
||||||
when {{lvl}}
|
|
||||||
@@severity = Log::Severity.new SEVERITY_IDS[{{i}}]
|
|
||||||
{% end %}
|
|
||||||
else
|
|
||||||
raise "Unknown log level #{level}"
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
@log = Log.for("")
|
|
||||||
@backend = Log::IOBackend.new
|
@backend = Log::IOBackend.new
|
||||||
|
|
||||||
format_proc = ->(entry : Log::Entry, io : IO) do
|
format_proc = ->(entry : Log::Entry, io : IO) do
|
||||||
@ -49,6 +37,24 @@ class Logger
|
|||||||
Log.setup @@severity, @backend
|
Log.setup @@severity, @backend
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.get_severity(level = "") : Log::Severity
|
||||||
|
if level.empty?
|
||||||
|
level = Config.current.log_level
|
||||||
|
end
|
||||||
|
{% begin %}
|
||||||
|
case level.downcase
|
||||||
|
when "off"
|
||||||
|
return Log::Severity::None
|
||||||
|
{% for lvl, i in LEVELS %}
|
||||||
|
when {{lvl}}
|
||||||
|
return Log::Severity.new SEVERITY_IDS[{{i}}]
|
||||||
|
{% end %}
|
||||||
|
else
|
||||||
|
raise "Unknown log level #{level}"
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
# Ignores @@severity and always log msg
|
# Ignores @@severity and always log msg
|
||||||
def log(msg)
|
def log(msg)
|
||||||
@backend.write Log::Entry.new "", Log::Severity::None, msg,
|
@backend.write Log::Entry.new "", Log::Severity::None, msg,
|
||||||
@ -61,7 +67,7 @@ class Logger
|
|||||||
|
|
||||||
{% for lvl in LEVELS %}
|
{% for lvl in LEVELS %}
|
||||||
def {{lvl.id}}(msg)
|
def {{lvl.id}}(msg)
|
||||||
@log.{{lvl.id}} { msg }
|
raw_log.{{lvl.id}} { msg }
|
||||||
end
|
end
|
||||||
def self.{{lvl.id}}(msg)
|
def self.{{lvl.id}}(msg)
|
||||||
default.not_nil!.{{lvl.id}} msg
|
default.not_nil!.{{lvl.id}} msg
|
||||||
|
@ -8,7 +8,7 @@ require "option_parser"
|
|||||||
require "clim"
|
require "clim"
|
||||||
require "tallboy"
|
require "tallboy"
|
||||||
|
|
||||||
MANGO_VERSION = "0.18.3"
|
MANGO_VERSION = "0.19.0"
|
||||||
|
|
||||||
# From http://www.network-science.de/ascii/
|
# From http://www.network-science.de/ascii/
|
||||||
BANNER = %{
|
BANNER = %{
|
||||||
@ -63,7 +63,12 @@ class CLI < Clim
|
|||||||
Plugin::Downloader.default
|
Plugin::Downloader.default
|
||||||
|
|
||||||
spawn do
|
spawn do
|
||||||
Server.new.start
|
begin
|
||||||
|
Server.new.start
|
||||||
|
rescue e
|
||||||
|
Logger.fatal e
|
||||||
|
Process.exit 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
MainFiber.start_and_block
|
MainFiber.start_and_block
|
||||||
|
@ -713,6 +713,24 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Koa.describe "Returns all tags"
|
||||||
|
Koa.response 200, ref: "$tagsResult"
|
||||||
|
get "/api/tags" do |env|
|
||||||
|
begin
|
||||||
|
tags = Storage.default.list_tags
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"tags" => tags,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Koa.describe "Adds a new tag to a title"
|
Koa.describe "Adds a new tag to a title"
|
||||||
Koa.path "tid", desc: "A title ID"
|
Koa.path "tid", desc: "A title ID"
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, ref: "$result"
|
||||||
|
@ -7,10 +7,6 @@ require "./routes/*"
|
|||||||
|
|
||||||
class Server
|
class Server
|
||||||
def initialize
|
def initialize
|
||||||
error 403 do |env|
|
|
||||||
message = "HTTP 403: You are not authorized to visit #{env.request.path}"
|
|
||||||
layout "message"
|
|
||||||
end
|
|
||||||
error 404 do |env|
|
error 404 do |env|
|
||||||
message = "HTTP 404: Mango cannot find the page #{env.request.path}"
|
message = "HTTP 404: Mango cannot find the page #{env.request.path}"
|
||||||
layout "message"
|
layout "message"
|
||||||
|
104
src/storage.cr
104
src/storage.cr
@ -3,6 +3,8 @@ require "crypto/bcrypt"
|
|||||||
require "uuid"
|
require "uuid"
|
||||||
require "base64"
|
require "base64"
|
||||||
require "./util/*"
|
require "./util/*"
|
||||||
|
require "mg"
|
||||||
|
require "../migration/*"
|
||||||
|
|
||||||
def hash_password(pw)
|
def hash_password(pw)
|
||||||
Crypto::Bcrypt::Password.create(pw).to_s
|
Crypto::Bcrypt::Password.create(pw).to_s
|
||||||
@ -13,9 +15,10 @@ def verify_password(hash, pw)
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Storage
|
class Storage
|
||||||
|
@@insert_ids = [] of IDTuple
|
||||||
|
|
||||||
@path : String
|
@path : String
|
||||||
@db : DB::Database?
|
@db : DB::Database?
|
||||||
@insert_ids = [] of IDTuple
|
|
||||||
|
|
||||||
alias IDTuple = NamedTuple(path: String,
|
alias IDTuple = NamedTuple(path: String,
|
||||||
id: String,
|
id: String,
|
||||||
@ -35,44 +38,15 @@ class Storage
|
|||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
begin
|
begin
|
||||||
# v0.18.0
|
MG::Migration.new(db, log: Logger.default.raw_log).migrate
|
||||||
db.exec "create table tags (id text, tag text, unique (id, tag))"
|
|
||||||
db.exec "create index tags_id_idx on tags (id)"
|
|
||||||
db.exec "create index tags_tag_idx on tags (tag)"
|
|
||||||
|
|
||||||
# v0.15.0
|
|
||||||
db.exec "create table thumbnails " \
|
|
||||||
"(id text, data blob, filename text, " \
|
|
||||||
"mime text, size integer)"
|
|
||||||
db.exec "create unique index tn_index on thumbnails (id)"
|
|
||||||
|
|
||||||
# v0.1.1
|
|
||||||
db.exec "create table ids" \
|
|
||||||
"(path text, id text, is_title integer)"
|
|
||||||
db.exec "create unique index path_idx on ids (path)"
|
|
||||||
db.exec "create unique index id_idx on ids (id)"
|
|
||||||
|
|
||||||
# v0.1.0
|
|
||||||
db.exec "create table users" \
|
|
||||||
"(username text, password text, token text, admin integer)"
|
|
||||||
rescue e
|
rescue e
|
||||||
unless e.message.not_nil!.ends_with? "already exists"
|
Logger.fatal "DB migration failed. #{e}"
|
||||||
Logger.fatal "Error when checking tables in DB: #{e}"
|
raise e
|
||||||
raise e
|
|
||||||
end
|
|
||||||
|
|
||||||
# If the DB is initialized through CLI but no user is added, we need
|
|
||||||
# to create the admin user when first starting the app
|
|
||||||
user_count = db.query_one "select count(*) from users", as: Int32
|
|
||||||
init_admin if init_user && user_count == 0
|
|
||||||
else
|
|
||||||
Logger.debug "Creating DB file at #{@path}"
|
|
||||||
db.exec "create unique index username_idx on users (username)"
|
|
||||||
db.exec "create unique index token_idx on users (token)"
|
|
||||||
|
|
||||||
init_admin if init_user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
user_count = db.query_one "select count(*) from users", as: Int32
|
||||||
|
init_admin if init_user && user_count == 0
|
||||||
|
|
||||||
# Verifies that the default username in config is valid
|
# Verifies that the default username in config is valid
|
||||||
if Config.current.disable_login
|
if Config.current.disable_login
|
||||||
username = Config.current.default_username
|
username = Config.current.default_username
|
||||||
@ -99,9 +73,11 @@ class Storage
|
|||||||
private def get_db(&block : DB::Database ->)
|
private def get_db(&block : DB::Database ->)
|
||||||
if @db.nil?
|
if @db.nil?
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
|
db.exec "PRAGMA foreign_keys = 1"
|
||||||
yield db
|
yield db
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
@db.not_nil!.exec "PRAGMA foreign_keys = 1"
|
||||||
yield @db.not_nil!
|
yield @db.not_nil!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -258,28 +234,38 @@ class Storage
|
|||||||
id = nil
|
id = nil
|
||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
get_db do |db|
|
get_db do |db|
|
||||||
id = db.query_one? "select id from ids where path = (?)", path,
|
if is_title
|
||||||
as: {String}
|
id = db.query_one? "select id from titles where path = (?)", path,
|
||||||
|
as: String
|
||||||
|
else
|
||||||
|
id = db.query_one? "select id from ids where path = (?)", path,
|
||||||
|
as: String
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_id(tp : IDTuple)
|
def insert_id(tp : IDTuple)
|
||||||
@insert_ids << tp
|
@@insert_ids << tp
|
||||||
end
|
end
|
||||||
|
|
||||||
def bulk_insert_ids
|
def bulk_insert_ids
|
||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
get_db do |db|
|
get_db do |db|
|
||||||
db.transaction do |tx|
|
db.transaction do |tran|
|
||||||
@insert_ids.each do |tp|
|
conn = tran.connection
|
||||||
tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path],
|
@@insert_ids.each do |tp|
|
||||||
tp[:id], tp[:is_title] ? 1 : 0
|
if tp[:is_title]
|
||||||
|
conn.exec "insert into titles values (?, ?, null)", tp[:id],
|
||||||
|
tp[:path]
|
||||||
|
else
|
||||||
|
conn.exec "insert into ids values (?, ?)", tp[:path], tp[:id]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@insert_ids.clear
|
@@insert_ids.clear
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -372,6 +358,7 @@ class Storage
|
|||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
Logger.info "Starting DB optimization"
|
Logger.info "Starting DB optimization"
|
||||||
get_db do |db|
|
get_db do |db|
|
||||||
|
# Delete dangling entry IDs
|
||||||
trash_ids = [] of String
|
trash_ids = [] of String
|
||||||
db.query "select path, id from ids" do |rs|
|
db.query "select path, id from ids" do |rs|
|
||||||
rs.each do
|
rs.each do
|
||||||
@ -380,29 +367,24 @@ class Storage
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delete dangling IDs
|
|
||||||
db.exec "delete from ids where id in " \
|
db.exec "delete from ids where id in " \
|
||||||
"(#{trash_ids.map { |i| "'#{i}'" }.join ","})"
|
"(#{trash_ids.map { |i| "'#{i}'" }.join ","})"
|
||||||
Logger.debug "#{trash_ids.size} dangling IDs deleted" \
|
Logger.debug "#{trash_ids.size} dangling entry IDs deleted" \
|
||||||
if trash_ids.size > 0
|
if trash_ids.size > 0
|
||||||
|
|
||||||
# Delete dangling thumbnails
|
# Delete dangling title IDs
|
||||||
trash_thumbnails_count = db.query_one "select count(*) from " \
|
trash_titles = [] of String
|
||||||
"thumbnails where id not in " \
|
db.query "select path, id from titles" do |rs|
|
||||||
"(select id from ids)", as: Int32
|
rs.each do
|
||||||
if trash_thumbnails_count > 0
|
path = rs.read String
|
||||||
db.exec "delete from thumbnails where id not in (select id from ids)"
|
trash_titles << rs.read String unless Dir.exists? path
|
||||||
Logger.info "#{trash_thumbnails_count} dangling thumbnails deleted"
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delete dangling tags
|
db.exec "delete from titles where id in " \
|
||||||
trash_tags_count = db.query_one "select count(*) from tags " \
|
"(#{trash_titles.map { |i| "'#{i}'" }.join ","})"
|
||||||
"where id not in " \
|
Logger.debug "#{trash_titles.size} dangling title IDs deleted" \
|
||||||
"(select id from ids)", as: Int32
|
if trash_titles.size > 0
|
||||||
if trash_tags_count > 0
|
|
||||||
db.exec "delete from tags where id not in (select id from ids)"
|
|
||||||
Logger.info "#{trash_tags_count} dangling tags deleted"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
Logger.info "DB optimization finished"
|
Logger.info "DB optimization finished"
|
||||||
end
|
end
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
# Web related helper functions/macros
|
# Web related helper functions/macros
|
||||||
|
|
||||||
|
# This macro defines `is_admin` when used
|
||||||
|
macro check_admin_access
|
||||||
|
is_admin = false
|
||||||
|
# The token (if exists) takes precedence over the default user option.
|
||||||
|
# this is why we check the default username first before checking the
|
||||||
|
# token.
|
||||||
|
if Config.current.disable_login
|
||||||
|
is_admin = Storage.default.
|
||||||
|
username_is_admin Config.current.default_username
|
||||||
|
end
|
||||||
|
if token = env.session.string? "token"
|
||||||
|
is_admin = Storage.default.verify_admin token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
macro layout(name)
|
macro layout(name)
|
||||||
base_url = Config.current.base_url
|
base_url = Config.current.base_url
|
||||||
|
check_admin_access
|
||||||
begin
|
begin
|
||||||
is_admin = false
|
|
||||||
# The token (if exists) takes precedence over the default user option.
|
|
||||||
# this is why we check the default username first before checking the
|
|
||||||
# token.
|
|
||||||
if Config.current.disable_login
|
|
||||||
is_admin = Storage.default.
|
|
||||||
username_is_admin Config.current.default_username
|
|
||||||
end
|
|
||||||
if token = env.session.string? "token"
|
|
||||||
is_admin = Storage.default.verify_admin token
|
|
||||||
end
|
|
||||||
page = {{name}}
|
page = {{name}}
|
||||||
render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr"
|
render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr"
|
||||||
rescue e
|
rescue e
|
||||||
@ -24,6 +29,15 @@ macro layout(name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
macro send_error_page(msg)
|
||||||
|
message = {{msg}}
|
||||||
|
base_url = Config.current.base_url
|
||||||
|
check_admin_access
|
||||||
|
page = "Error"
|
||||||
|
html = render "src/views/message.html.ecr", "src/views/layout.html.ecr"
|
||||||
|
send_file env, html.to_slice, "text/html"
|
||||||
|
end
|
||||||
|
|
||||||
macro send_img(env, img)
|
macro send_img(env, img)
|
||||||
send_file {{env}}, {{img}}.data, {{img}}.mime
|
send_file {{env}}, {{img}}.data, {{img}}.mime
|
||||||
end
|
end
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
<div class="uk-margin" x-data="tagsComponent()" x-cloak x-init="load(<%= is_admin %>)">
|
|
||||||
<p class="uk-text-meta" @selectstart.prevent>
|
|
||||||
<span style="position:relative; bottom:3px; margin-right:5px;">Tags: </span>
|
|
||||||
<template x-for="tag in tags" :key="tag">
|
|
||||||
<span class="uk-label uk-label-primary" style="padding:2px 5px; margin:0 5px 5px 5px; text-transform:none;">
|
|
||||||
<a class="uk-link-reset" x-show="isAdmin" @click="rm($event)" :id="`${tag}-rm`"><span uk-icon="close" style="margin-right: 5px; position: relative; bottom: 1.5px;"></span></a><a class="uk-link-reset" x-text="tag" :href="`<%= base_url %>tags/${encodeURIComponent(tag)}`"></a>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<a class="uk-link-reset" style="position:relative; bottom:3px;" :uk-icon="inputShown ? 'close' : 'plus'" @click="toggleInput($nextTick)" x-show="isAdmin"></a>
|
|
||||||
</p>
|
|
||||||
<input id="tag-input" class="uk-input" type="text" placeholder="Type in a new tag and hit enter" x-model="newTag" @keydown="keydown($event)" x-show="inputShown">
|
|
||||||
</div>
|
|
@ -43,7 +43,7 @@
|
|||||||
<template x-if="job.status_message.length > 0">
|
<template x-if="job.status_message.length > 0">
|
||||||
<div class="uk-inline">
|
<div class="uk-inline">
|
||||||
<span uk-icon="info"></span>
|
<span uk-icon="info"></span>
|
||||||
<div uk-dropdown x-text="job.status_message"></div>
|
<div uk-dropdown x-text="job.status_message" style="white-space: pre-line;"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
|
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-navbar-left uk-visible@s">
|
<div class="uk-navbar-left uk-visible@s">
|
||||||
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png"></a>
|
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png" style="width:90px;height:90px;"></a>
|
||||||
<ul class="uk-navbar-nav">
|
<ul class="uk-navbar-nav">
|
||||||
<li><a href="<%= base_url %>">Home</a></li>
|
<li><a href="<%= base_url %>">Home</a></li>
|
||||||
<li><a href="<%= base_url %>library">Library</a></li>
|
<li><a href="<%= base_url %>library">Library</a></li>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="uk-section uk-section-small">
|
<div class="uk-section uk-section-small">
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-section uk-section-small" id="main-section">
|
<div class="uk-section uk-section-small" style="position:relative;">
|
||||||
<div class="uk-container uk-container-small">
|
<div class="uk-container uk-container-small">
|
||||||
<div id="alert"></div>
|
<div id="alert"></div>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html class="reader-bg">
|
<html style="background-color: black;">
|
||||||
|
|
||||||
<% page = "Reader" %>
|
<% page = "Reader" %>
|
||||||
<%= render_component "head" %>
|
<%= render_component "head" %>
|
||||||
|
@ -34,7 +34,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p class="uk-text-meta"><%= title.content_label %> found</p>
|
<p class="uk-text-meta"><%= title.content_label %> found</p>
|
||||||
|
|
||||||
<%= render_component "tags" %>
|
<div class="uk-margin" x-data="tagsComponent()" x-cloak x-init="load(<%= is_admin %>)" x-show="!loading">
|
||||||
|
<select class="tag-select" multiple="multiple" style="width:100%">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="uk-grid-small" uk-grid>
|
<div class="uk-grid-small" uk-grid>
|
||||||
<div class="uk-margin-bottom uk-width-3-4@s">
|
<div class="uk-margin-bottom uk-width-3-4@s">
|
||||||
@ -121,6 +124,9 @@
|
|||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "dots-scripts" %>
|
<%= render_component "dots-scripts" %>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<link href="/css/tags.css" rel="stylesheet" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/title.js"></script>
|
<script src="<%= base_url %>js/title.js"></script>
|
||||||
<script src="<%= base_url %>js/search.js"></script>
|
<script src="<%= base_url %>js/search.js"></script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user