Update UI

This commit is contained in:
Radhi Fadlillah 2018-05-18 14:07:15 +07:00
parent d01f87b358
commit 5232e0e2c9
15 changed files with 926 additions and 1405 deletions

File diff suppressed because one or more lines are too long

View File

@ -97,18 +97,11 @@ func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, p
err = json.NewDecoder(r.Body).Decode(&book)
checkError(err)
// Make sure URL valid
parsedURL, err := nurl.ParseRequestURI(book.URL)
if err != nil || parsedURL.Host == "" {
panic(fmt.Errorf("URL is not valid"))
}
// Clear UTM parameters from URL
book.URL = clearUTMParams(parsedURL)
// Fetch data from internet
article, _ := readability.Parse(book.URL, 10*time.Second)
article, err := readability.Parse(book.URL, 20*time.Second)
checkError(err)
book.URL = article.URL
book.ImageURL = article.Meta.Image
book.Author = article.Meta.Author
book.MinReadTime = article.Meta.MinReadTime
@ -116,18 +109,9 @@ func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, p
book.Content = article.Content
book.HTML = article.RawContent
// If title and excerpt doesnt have submitted value, use from article
if book.Title == "" {
book.Title = article.Meta.Title
}
if book.Excerpt == "" {
book.Excerpt = article.Meta.Excerpt
}
// Make sure title is not empty
if book.Title == "" {
book.Title = "Untitled"
book.Title = book.URL
}
// Save to database

View File

@ -28,6 +28,8 @@ func newWebHandler(db dt.Database) (*webHandler, error) {
return nil, err
}
jwtKey = []byte("Test12345")
// Create templates
funcMap := template.FuncMap{
"html": func(s string) template.HTML {

File diff suppressed because one or more lines are too long

1
view/css/yla-dialog.css Normal file
View File

@ -0,0 +1 @@
.header-link{border-right:1px solid #E5E5E5;color:#232323;cursor:pointer;font-size:.9em;line-height:60px;overflow:hidden;padding:0 16px}.header-link:hover{color:#F44336}.full-overlay{position:fixed;z-index:101;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column;background-color:rgba(0,0,0,0.5);top:0;left:0;right:0;bottom:0;overflow:hidden;-webkit-box-pack:center;justify-content:center;padding:32px}.yla-dialog__overlay{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;min-width:0;min-height:0;overflow:hidden;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background-color:rgba(0,0,0,0.6);padding:32px}.yla-dialog__overlay .yla-dialog{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;min-width:400px;min-height:0;max-height:100%;overflow:hidden;background-color:#FFF;font-size:16px}.yla-dialog__overlay .yla-dialog>.yla-dialog__header{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:16px;min-height:0;background-color:#353535;color:#EEE;flex-shrink:0}.yla-dialog__overlay .yla-dialog>.yla-dialog__header>p{-webkit-box-flex:1;flex:1 0;font-weight:600;font-size:1em;text-transform:uppercase}.yla-dialog__overlay .yla-dialog>.yla-dialog__header>a:hover{color:#F44336}.yla-dialog__overlay .yla-dialog>.yla-dialog__body{padding:16px;display:grid;max-height:100%;min-height:80px;min-width:0;font-size:.9em;overflow:auto;grid-template-columns:max-content 1fr;-webkit-box-align:baseline;align-items:baseline;grid-gap:16px}.yla-dialog__overlay .yla-dialog>.yla-dialog__body>.yla-dialog__content{grid-column-start:1;grid-column-end:3;align-self:baseline}.yla-dialog__overlay .yla-dialog>.yla-dialog__body>input{color:#232323;padding:8px;border:1px solid #E5E5E5}.yla-dialog__overlay .yla-dialog>.yla-dialog__body>.suggestion{position:absolute;display:block;padding:8px;background-color:#EEE;border:1px solid #E5E5E5}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer{padding:16px;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;-webkit-box-pack:end;justify-content:flex-end;border-top:1px solid #E5E5E5}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>a{text-transform:uppercase;padding:0 8px;font-size:.9em;font-weight:600}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>a:hover{color:#F44336}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>a:focus{outline:none;color:#F44336;border-bottom:1px dashed #F44336}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>i{width:19px;line-height:19px;text-align:center}

1
view/css/yla-tooltip.css Normal file
View File

@ -0,0 +1 @@
.header-link{border-right:1px solid #E5E5E5;color:#232323;cursor:pointer;font-size:.9em;line-height:70px;overflow:hidden;padding:0 16px}.header-link:hover{color:#F44336}.full-overlay{position:fixed;z-index:101;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column;background-color:rgba(0,0,0,0.5);top:0;left:0;right:0;bottom:0;overflow:hidden;-webkit-box-pack:center;justify-content:center;padding:32px}.yla-tooltip{font-size:14px;color:#EEE;background-color:#232323;padding:8px;border-radius:4px;position:relative;z-index:1000}.yla-tooltip::after{content:'';display:block;position:absolute;border:8px solid transparent}.yla-tooltip.left{margin-left:-14px}.yla-tooltip.left::after{top:50%;right:-16px;margin-top:-8px;border-left-color:#232323}.yla-tooltip.top{margin-top:-14px}.yla-tooltip.top::after{left:50%;bottom:-16px;margin-left:-8px;border-top-color:#232323}.yla-tooltip.right{margin-left:14px}.yla-tooltip.right::after{top:50%;left:-16px;margin-top:-8px;border-right-color:#232323}.yla-tooltip.bottom{margin-top:14px}.yla-tooltip.bottom::after{left:50%;top:-16px;margin-left:-8px;border-bottom-color:#232323}

View File

@ -5,12 +5,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/css/stylesheet.css">
<link rel="stylesheet" href="/css/fontawesome.css">
<link rel="stylesheet" href="/css/source-sans-pro.css">
<link rel="stylesheet" href="/css/yla-dialog.css">
<link rel="stylesheet" href="/css/yla-tooltip.css">
<link rel="stylesheet" href="/css/stylesheet.css">
<script src="/js/vue.js"></script>
<script src="/js/axios.js"></script>
<script src="/js/js-cookie.js"></script>
<script src="/js/component/yla-tooltip.js"></script>
<script src="/js/component/yla-dialog.js"></script>
<script src="/js/page/base.js"></script>
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32" />
@ -19,653 +24,146 @@
</head>
<body>
<div id="main-page">
<div id="header">
<template v-if="checkedBookmarks.length === 0">
<a id="logo" href="/">
<span></span>shiori</a>
<div id="search-box">
<input type="text" name="keyword" v-model.trim="search.query" @keyup.enter="loadData" placeholder="Search url, tags, title or content">
<a class="button" @click="loadData">
<i class="fas fa-search fa-fw"></i>
</a>
</div>
<div id="header-menu" v-if="!loading">
<a @click="reloadData">
<i class="fas fa-cloud fa-fw"></i>
<span>Reload</span>
</a>
<a @click="showTagCloud">
<i class="fas fa-hashtag fa-fw"></i>
<span>Tags</span>
</a>
<a @click="toggleImage">
<i class="fas fa-fw" :class="showImage ? 'fa-eye-slash' : 'fa-eye'"></i>
<span>{{showImage ? 'Hide image' : 'Show image'}}</span>
</a>
<a @click="logout">
<i class="fas fa-sign-out-alt fa-fw"></i>
<span>Logout</span>
</a>
</div>
</template>
<template v-else>
<p id="n-selected">{{checkedBookmarks.length}} selected</p>
<div id="header-menu">
<a @click="clearSelectedBookmarks">
<i class="fas fa-fw fa-ban"></i>
<span>Cancel</span>
</a>
<a @click="selectAllBookmarks">
<i class="fas fa-fw fa-check-square"></i>
<span>Select all</span>
</a>
<a @click="deleteBookmarks(checkedBookmarks)">
<i class="fas fa-fw fa-trash"></i>
<span>Delete</span>
</a>
</div>
</template>
<div id="index-page">
<div id="sidebar">
<p id="logo"></p>
<yla-tooltip placement="right" content="Reload data">
<a>
<i class="fas fa-sync-alt fa-fw"></i>
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Add new bookmark">
<a @click="showDialogAdd">
<i class="fas fa-plus fa-fw"></i>
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Batch edit">
<a>
<i class="fas fa-pencil-alt fa-fw"></i>
</a>
</yla-tooltip>
<div class="spacer"></div>
<yla-tooltip placement="right" content="Toggle night mode">
<a>
<i class="fas fa-moon fa-fw"></i>
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Log out">
<a>
<i class="fas fa-sign-out-alt fa-fw"></i>
</a>
</yla-tooltip>
</div>
<div id="main">
<template v-if="!loading && error === ''">
<div id="input-bookmark">
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
<input type="text" ref="inputURL" v-model.trim="inputBookmark.url" placeholder="URL for the new bookmark" @focus="clearSelectedBookmarks">
<template v-if="inputBookmark.url !== ''">
<input type="text" v-model.trim="inputBookmark.title" placeholder="Custom bookmark title (optional)">
<input type="text" v-model.trim="inputBookmark.tags" placeholder="Space separated tags for this bookmark (optional)">
<textarea name="excerpt" v-model.trim="inputBookmark.excerpt" placeholder="Excerpt for this bookmark (optional)"></textarea>
<p v-if="inputBookmark.error !== ''" class="error-message">{{inputBookmark.error}}</p>
<div class="button-area">
<div class="spacer"></div>
<a v-if="inputBookmark.loading">
<i class="fas fa-fw fa-spinner fa-spin"></i>
</a>
<template v-else>
<a class="button" @click="clearInputBookmark">Cancel</a>
<a class="button" @click="saveBookmark">Done</a>
</template>
</div>
</template>
</div>
<div v-if="search.query !== '' && !loading" id="search-parameter">
<a v-if="search.keyword !== ''" @click="removeSearchParam(search.keyword)">{{search.keyword}}</a>
<a v-for="tag in search.tags" @click="removeSearchParam('#'+tag)">#{{tag}}</a>
</div>
<div id="grid">
<div v-for="column in gridColumns" class="column" :style="{maxWidth: columnWidth}">
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
<a class="checkbox" @click="toggleBookmarkCheck(item.index)">
<i class="fas fa-check"></i>
</a>
<a class="bookmark-metadata" target="_blank" :class="{'has-image':bookmarkImage(item) !== ''}" :style="bookmarkImage(item)" :href="item.url">
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
<p class="bookmark-title">{{item.title}}</p>
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
</a>
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
<div v-if="item.tags.length > 0" class="bookmark-tags">
<a v-for="tag in item.tags" @click="searchTag(tag.name)">{{tag.name}}</a>
</div>
<div class="bookmark-menu">
<a @click="updateBookmark(item.index)">
<i class="fas fa-sync"></i>
<span>Update</span>
</a>
<a @click="editBookmark(item.index)">
<i class="fas fa-pencil-alt"></i>
<span>Edit</span>
</a>
<a @click="deleteBookmarks([item.index])">
<i class="far fa-trash-alt"></i>
<span>Delete</span>
</a>
<a :href="'/bookmark/'+item.id" target="_blank">
<i class="fas fa-history"></i>
<span>Cache</span>
</a>
</div>
</div>
<div id="body">
<div id="header">
<input type="text" placeholder="Search bookmarks by url, tags, title or content">
<a>
<i class="fas fa-search fa-fw"></i>
</a>
</div>
<div id="grid">
<div class="bookmark" v-for="book in bookmarks">
<a class="bookmark-content" :href="'/bookmark/'+book.id" target="_blank">
<img v-if="book.imageURL !== ''" :src="book.imageURL">
<p class="title">{{book.title}}</p>
<p class="excerpt" v-if="book.imageURL === ''">{{book.excerpt}}</p>
</a>
<div class="bookmark-menu">
<a class="url" title="View original" :href="book.url" target="_blank">
{{getHostname(book.url)}}
</a>
<a title="Edit bookmark">
<i class="fas fa-pencil-alt"></i>
</a>
<a title="Edit tags">
<i class="fas fa-tags"></i>
</a>
<a title="Delete bookmark">
<i class="fas fa-trash-alt"></i>
</a>
<a title="Update cache">
<i class="fas fa-cloud-download-alt"></i>
</a>
</div>
</div>
</template>
<div v-if="loading || error !== ''" id="message-bar">
<i v-if="loading" class="fas fa-fw fa-spinner fa-spin"></i>
<p v-if="error !== ''" class="error-message">{{error}}</p>
</div>
</div>
<div v-if="dialog.visible" id="dialog-overlay">
<div id="dialog">
<p id="dialog-title" :class="{'error-message': dialog.isError}">{{dialog.title}}</p>
<p v-html="dialog.content" id="dialog-content"></p>
<div id="dialog-button">
<div class="spacer"></div>
<a v-if="dialog.loading">
<i class="fas fa-fw fa-spinner fa-spin"></i>
</a>
<template v-else>
<a class="button" @click="dialog.secondAction">{{dialog.secondChoice}}</a>
<a class="button" @click="dialog.mainAction">{{dialog.mainChoice}}</a>
</template>
</div>
</div>
</div>
<div v-if="tagCloud.visible" id="tag-cloud-overlay">
<div id="tag-cloud">
<div id="tag-cloud-title">
<p>Tag Cloud</p>
<a @click="tagCloud.visible = false" id="close-tag-cloud">
<i class="fas fa-fw fa-times"></i>
</a>
</div>
<div id="tag-cloud-content">
<a v-for="item in tagCloud.data" @click="selectTagCloud(item.name)" :style="{fontSize: item.fontSize+'em'}">#{{item.name}}</a>
</div>
</div>
</div>
<yla-dialog v-bind="dialog"></yla-dialog>
</div>
<script>
// Prepare axios instance
var token = Cookies.get('token'),
instance = axios.create();
rest = axios.create();
instance.defaults.timeout = 10000;
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token;
rest.defaults.timeout = 15000;
rest.defaults.headers.common['Authorization'] = 'Bearer ' + token;
var app = new Vue({
el: '#main-page',
// Register Vue component
Vue.component('yla-dialog', new YlaDialog());
Vue.component('yla-tooltip', new YlaTooltip());
new Vue({
el: '#index-page',
mixins: [new Base()],
data: {
windowWidth: 0,
error: "",
loading: false,
displayTags: false,
bookmarks: [],
tags: [],
checkedBookmarks: [],
showImage: true,
search: {
query: "",
keyword: "",
tags: []
},
inputBookmark: {
index: -1,
id: -1,
url: "",
title: "",
tags: "",
excerpt: "",
error: "",
loading: false
},
dialog: {
visible: false,
loading: false,
isError: false,
title: '',
content: '',
mainChoice: '',
secondChoice: '',
mainAction: function () {},
secondAction: function () {}
},
tagCloud: {
visible: false,
data: []
}
},
methods: {
searchTag: function (tag) {
if (this.loading) return;
var newTag = '#' + tag;
if (this.search.query.indexOf(newTag) === -1) {
this.search.query += ' ' + newTag;
this.search.query = this.search.query.trim().replace(/\s+/g, ' ');
this.loadData();
}
},
removeSearchParam: function (param) {
if (this.loading) return;
this.search.query = this.search.query.replace(param, ' ').trim().replace(/\s+/g, ' ');
this.loadData();
},
reloadData: function () {
if (this.loading) return;
this.search.query = '';
this.loadData();
},
loadData: function () {
if (this.loading) return;
// Parse search query
var rxTags = /(^|\s+)#(\S+)/g,
tags = [];
while ((result = rxTags.exec(this.search.query)) !== null) {
tags.push(result[2]);
}
var keyword = this.search.query.replace(/(^|\s+)#(\S+)/g, ' ').trim().replace(/\s+/g, ' ');
// Fetch data
this.error = '';
this.loading = true;
this.search.tags = tags;
this.search.keyword = keyword;
instance.get('/api/bookmarks', {
params: {
keyword: this.search.keyword,
tags: this.search.tags.join(" ")
}
})
.then(function (response) {
app.loading = false;
app.bookmarks = response.data;
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.loading = false;
app.error = errorMsg.trim();
});
},
saveBookmark: function () {
if (this.inputBookmark.loading) return;
this.inputBookmark.loading = true;
if (this.inputBookmark.url === "") return;
var idx = this.inputBookmark.index,
tags = this.inputBookmark.tags.replace(/\s+/g, " "),
newTags = tags === "" ? [] : listTag = tags.split(/\s+/g),
finalTags = [];
if (idx !== -1) {
var oldTags = this.bookmarks[idx].tags;
for (var i = 0; i < oldTags.length; i++) {
if (newTags.indexOf(oldTags[i].name) === -1) {
finalTags.push({
name: '-' + oldTags[i].name
})
}
}
}
for (var i = 0; i < newTags.length; i++) {
finalTags.push({
name: listTag[i]
});
}
instance.request({
method: this.inputBookmark.id === -1 ? 'post' : 'put',
url: '/api/bookmarks',
timeout: 15000,
data: {
id: this.inputBookmark.id,
url: this.inputBookmark.url,
title: this.inputBookmark.title,
excerpt: this.inputBookmark.excerpt,
tags: finalTags
}
})
.then(function (response) {
if (idx === -1) app.bookmarks.unshift(response.data);
else {
app.bookmarks.splice(idx, 1, response.data);
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
}
app.clearInputBookmark();
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.inputBookmark.loading = false;
app.inputBookmark.error = errorMsg.trim();
});
},
editBookmark: function (idx) {
var bookmark = this.bookmarks[idx],
tags = [];
for (var i = 0; i < bookmark.tags.length; i++) {
tags.push(bookmark.tags[i].name);
}
this.inputBookmark.index = idx;
this.inputBookmark.id = bookmark.id;
this.inputBookmark.url = bookmark.url;
this.inputBookmark.title = bookmark.title;
this.inputBookmark.tags = tags.join(" ");
this.inputBookmark.excerpt = bookmark.excerpt;
this.$nextTick(function () {
window.scrollTo(0, 0);
app.$refs.inputURL.focus();
});
},
deleteBookmarks: function (indices) {
var title = "Delete Bookmarks",
content = "Delete the selected bookmark(s) ? This action is irreversible.",
smallestIndex = 1;
if (indices.length === 0) return;
else if (indices.length === 1) {
var bookmark = this.bookmarks[indices[0]];
smallestIndex = indices[0];
title = "Delete Bookmark";
content = "Delete <b>\"" + bookmark.title.trim() + "\"</b> from bookmarks ? This action is irreversible.";
} else {
indices.sort();
smallestIndex = indices[indices.length - 1];
}
this.dialog.visible = true;
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = title;
this.dialog.content = content;
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.loading = true;
var listId = [];
for (var i = 0; i < indices.length; i++) {
listId.push('' + app.bookmarks[indices[i]].id);
}
instance.delete('/api/bookmarks/', {
data: listId
})
.then(function (response) {
app.dialog.loading = false;
app.dialog.visible = false;
for (var i = indices.length - 1; i >= 0; i--) {
app.bookmarks.splice(indices[i], 1);
}
app.clearSelectedBookmarks();
var scrollIdx = smallestIndex === 1 ? 1 : smallestIndex - 1;
app.$nextTick(function () {
var el = app.$refs['bookmark-' + smallestIndex][0];
if (el) el.scrollIntoView();
else window.scrollTo(0, 0);
});
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.showDialogError("Error Deleting Bookmark", errorMsg.trim());
});
};
this.dialog.secondAction = function () {
app.dialog.visible = false;
app.$nextTick(function () {
app.$refs['bookmark-' + smallestIndex][0].scrollIntoView();
});
};
},
updateBookmark: function (idx) {
var bookmark = this.bookmarks[idx],
sendUpdateRequest = function (overwrite) {
var url = "/api/bookmarks";
if (!overwrite) url += "?dont-overwrite";
instance.put(url, {
id: bookmark.id,
excerpt: overwrite ? "empty" : bookmark.excerpt
}, {
timeout: 15000,
})
.then(function (response) {
app.dialog.loading = false;
app.dialog.visible = false;
app.bookmarks.splice(idx, 1, response.data);
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.showDialogError("Error Updating Bookmark", errorMsg.trim());
});
};
this.dialog.visible = true;
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = "Update Bookmark";
this.dialog.content = "Update data of <b>\"" + bookmark.title.trim() + "\"</b> ? This action is irreversible.";
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.title = "Overwrite Metadata";
app.dialog.content = "Overwrite the existing bookmark's metadata ?";
app.dialog.mainChoice = "Yes";
app.dialog.secondChoice = "No";
app.dialog.mainAction = function () {
app.dialog.loading = true;
sendUpdateRequest(true);
};
app.dialog.secondAction = function () {
app.dialog.loading = true;
sendUpdateRequest(false);
};
};
this.dialog.secondAction = function () {
app.dialog.visible = false;
app.$nextTick(function () {
app.$refs['bookmark-' + idx][0].scrollIntoView();
});
};
},
toggleBookmarkCheck: function (idx) {
var checkedIdx = this.checkedBookmarks.indexOf(idx);
if (checkedIdx !== -1) this.checkedBookmarks.splice(checkedIdx, 1);
else this.checkedBookmarks.push(idx);
},
selectAllBookmarks: function () {
this.clearSelectedBookmarks();
for (var i = 0; i < this.bookmarks.length; i++) {
this.checkedBookmarks.push(i);
}
},
clearSelectedBookmarks: function () {
this.checkedBookmarks.splice(0, this.checkedBookmarks.length);
},
isBookmarkChecked: function (idx) {
return this.checkedBookmarks.indexOf(idx) !== -1;
},
clearInputBookmark: function () {
var idx = this.inputBookmark.index;
this.inputBookmark.index = -1;
this.inputBookmark.id = -1;
this.inputBookmark.url = "";
this.inputBookmark.title = "";
this.inputBookmark.tags = "";
this.inputBookmark.excerpt = "";
this.inputBookmark.error = "";
this.inputBookmark.loading = false;
if (idx !== -1) app.$nextTick(function () {
var bookmarkItem = app.$refs['bookmark-' + idx];
bookmarkItem[0].scrollIntoView();
})
},
showTagCloud: function () {
loadData() {
if (this.loading) return;
// Fetch data
this.error = '';
this.loading = true;
instance.get('/api/tags')
.then(function (response) {
app.loading = false;
app.tagCloud.data = response.data;
if (app.tagCloud.data.length === 0) {
app.tagCloud.visible = false;
app.showDialogError("Error Creating Tag Cloud", "There are no saved tags");
return;
}
app.tagCloud.visible = true;
// Find largest tags frequency
var minFont = 1,
maxFont = 5,
maxCount = 0;
for (var i = 0; i < app.tagCloud.data.length; i++) {
if (app.tagCloud.data[i].nBookmarks > maxCount) {
maxCount = app.tagCloud.data[i].nBookmarks;
}
}
// Set font size for tags
for (var i = 0; i < app.tagCloud.data.length; i++) {
var size = (Math.log(app.tagCloud.data[i].nBookmarks) / Math.log(maxCount)) *
(maxFont - minFont) + minFont;
app.tagCloud.data[i].fontSize = size;
}
rest.get('/api/bookmarks')
.then((response) => {
this.loading = false;
this.bookmarks = response.data;
console.log(JSON.stringify(response.data));
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.loading = false;
app.tagCloud.visible = false;
app.showDialogError("Error Creating Tag Cloud", errorMsg.trim());
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.loading = false;
this.showErrorDialog(errorMsg);
});
},
selectTagCloud: function (tag) {
this.tagCloud.visible = false;
this.searchTag(tag);
},
bookmarkTime: function (book) {
// Define read time
var readTime = "";
if (book.maxReadTime === 0) {
readTime = "";
} else if (book.minReadTime === book.maxReadTime) {
readTime = book.minReadTime + " min read";
} else {
readTime = book.minReadTime + "-" + book.maxReadTime + " min read";
}
// Convert modified time to local
var time = new Date(book.modified.replace(/-/g, '/') + ' +00');
// Create final time
var month = ("00" + (time.getMonth() + 1)).slice(-2),
date = ("00" + time.getDate()).slice(-2),
hours = ("00" + time.getHours()).slice(-2),
minutes = ("00" + time.getMinutes()).slice(-2),
seconds = ("00" + time.getSeconds()).slice(-2);
var finalBookmarkTime = "Updated " +
time.getFullYear() + "-" + month + "-" + date + " " +
hours + ":" + minutes + ":" + seconds;
if (readTime !== "") finalBookmarkTime += " \u00B7 " + readTime;
return finalBookmarkTime;
},
toggleImage: function () {
this.showImage = !this.showImage;
if (this.showImage) localStorage.setItem('show-image', '');
else localStorage.removeItem('show-image');
},
bookmarkImage: function (book) {
if (!this.showImage) return "";
if (book.imageURL === "") return "";
return "background-image: url(" + book.imageURL + ")";
},
getDomainURL: function (url) {
var hostname;
if (url.indexOf("://") > -1) {
hostname = url.split('/')[2];
} else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0];
hostname = hostname.split('?')[0];
return hostname;
},
showDialogError: function (title, msg) {
this.dialog.isError = true;
this.dialog.visible = true;
this.dialog.loading = false;
this.dialog.title = title;
this.dialog.content = msg;
this.dialog.mainChoice = "OK"
this.dialog.secondChoice = ""
this.dialog.mainAction = function () {
app.dialog.visible = false;
}
this.dialog.secondAction = function () {}
},
logout: function () {
Cookies.remove('token');
location.href = '/login';
}
},
computed: {
overlayVisible: function () {
return this.dialog.visible || this.tagCloud.visible;
},
gridColumns: function () {
var nColumn = Math.round(this.windowWidth / 500),
finalContent = [],
currentColumn = 0;
for (var i = 0; i < nColumn; i++) {
finalContent.push([]);
}
for (var i = 0; i < this.bookmarks.length; i++) {
var bookmark = this.bookmarks[i];
bookmark.index = i;
finalContent[currentColumn].push(bookmark);
currentColumn += 1;
if (currentColumn >= nColumn) currentColumn = 0;
}
return finalContent;
},
columnWidth: function () {
var nColumn = Math.round(this.windowWidth / 500),
percent = Math.round(100 / nColumn * 1000) / 1000;
return percent + "%";
}
},
watch: {
overlayVisible: function (isVisible) {
if (isVisible) document.body.className = "noscroll";
else document.body.removeAttribute("class");
},
'inputBookmark.url': function (newURL) {
if (newURL === "") this.clearInputBookmark();
else this.$nextTick(function () {
app.$refs.inputURL.focus();
showDialogAdd() {
this.showDialog({
title: 'New Bookmark',
content: 'Save an URL to bookmark',
fields: [{
name: 'url',
label: 'http://...',
}],
mainText: 'OK',
secondText: 'Cancel',
mainClick: (data) => {
this.dialog.loading = true;
rest.post('/api/bookmarks', {
url: data.url,
})
.then((response) => {
this.dialog.loading = false;
this.dialog.visible = false;
this.bookmarks.unshift(response.data);
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
});
}
});
},
getHostname(url) {
parser = document.createElement('a');
parser.href = url;
return parser.hostname;
}
},
mounted: function () {
this.showImage = localStorage.getItem('show-image') !== null;
this.windowWidth = window.innerWidth;
window.addEventListener('resize', function () {
app.windowWidth = window.innerWidth;
})
mounted() {
this.loadData();
}
})
});
</script>
</body>

View File

@ -0,0 +1,215 @@
var YlaDialog = function () {
// Private variable
var _template = `
<div v-if="visible" class="yla-dialog__overlay">
<div class="yla-dialog">
<div class="yla-dialog__header">
<p>{{title}}</p>
</div>
<div class="yla-dialog__body">
<slot>
<p class="yla-dialog__content">{{content}}</p>
<template v-for="(field,index) in formFields">
<p v-if="showLabel">{{field.label}} :</p>
<input :style="{gridColumnEnd: showLabel ? null : 'span 2'}"
:type="fieldType(field)"
:placeholder="field.label"
:tabindex="index+1"
ref="input"
v-model="field.value"
@focus="$event.target.select()"
@keyup="handleInput(index)"
@keyup.enter="handleInputEnter(index)">
<span ref="suggestion" v-if="field.suggestion" class="suggestion">{{field.suggestion}}</span>
</template>
</slot>
</div>
<div class="yla-dialog__footer">
<i v-if="loading" class="fas fa-fw fa-spinner fa-spin"></i>
<slot v-else name="custom-footer">
<a v-if="secondText"
:tabindex="btnTabIndex+1"
@click="handleSecondClick"
@keyup.enter="handleSecondClick"
class="yla-dialog__button">{{secondText}}
</a>
<a :tabindex="btnTabIndex"
ref="mainButton"
@click="handleMainClick"
@keyup.enter="handleMainClick"
class="yla-dialog__button main">{{mainText}}
</a>
</slot>
</div>
</div>
</div>`;
return {
template: _template,
props: {
visible: Boolean,
loading: Boolean,
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
fields: {
type: Array,
default () {
return []
}
},
showLabel: {
type: Boolean,
default: false
},
mainText: {
type: String,
default: 'OK'
},
secondText: String,
mainClick: {
type: Function,
default () {}
},
secondClick: {
type: Function,
default () {}
}
},
data() {
return {
formFields: []
};
},
computed: {
btnTabIndex() {
return this.fields.length + 1;
}
},
watch: {
fields: {
immediate: true,
handler() {
this.formFields = this.fields.map(field => {
if (typeof field === 'string') return {
name: field,
label: field,
value: '',
type: 'text',
dictionary: [],
suggestion: undefined
}
if (typeof field === 'object') return {
name: field.name || '',
label: field.label || '',
value: field.value || '',
type: field.type || 'text',
dictionary: field.dictionary instanceof Array ? field.dictionary : [],
suggestion: undefined
}
});
}
},
'fields.length' () {
this.focus();
},
visible() {
this.focus();
}
},
methods: {
fieldType(f) {
var type = f.type || 'text';
if (type !== 'text' && type !== 'password') return 'text';
else return type;
},
handleMainClick() {
var data = {};
this.formFields.forEach(field => {
var value = field.value;
if (field.type === 'number') value = parseInt(value, 10) || 0;
else if (field.type === 'float') value = parseFloat(value) || 0.0;
data[field.name] = value;
})
this.mainClick(data);
},
handleSecondClick() {
this.secondClick();
},
handleInput(index) {
// Create initial variable
var field = this.formFields[index],
dictionary = field.dictionary;
// Make sure dictionary is not empty
if (dictionary.length === 0) return;
// Fetch suggestion from dictionary
var words = field.value.split(' '),
lastWord = words[words.length - 1].toLowerCase(),
suggestion;
if (lastWord !== '') {
suggestion = dictionary.find(word => {
return word.toLowerCase().startsWith(lastWord)
});
}
this.formFields[index].suggestion = suggestion;
// Make sure suggestion exist
if (suggestion == null) return;
// Display suggestion
this.$nextTick(() => {
var input = this.$refs.input[index],
span = this.$refs.suggestion[index],
inputRect = input.getBoundingClientRect();
span.style.top = (inputRect.bottom - 1) + 'px';
span.style.left = inputRect.left + 'px';
});
},
handleInputEnter(index) {
var suggestion = this.formFields[index].suggestion;
if (suggestion == null) {
this.handleMainClick();
return;
}
var words = this.formFields[index].value.split(' ');
words.pop();
words.push(suggestion);
this.formFields[index].value = words.join(' ') + ' ';
this.formFields[index].suggestion = undefined;
},
focus() {
this.$nextTick(() => {
if (!this.visible) return;
var fields = this.$refs.input,
otherInput = this.$el.querySelectorAll('input'),
button = this.$refs.mainButton;
if (fields && fields.length > 0) {
this.$refs.input[0].focus();
this.$refs.input[0].select();
} else if (otherInput && otherInput.length > 0) {
otherInput[0].focus();
otherInput[0].select();
} else if (button) {
button.focus();
}
});
}
}
};
};

View File

@ -0,0 +1,106 @@
var YlaTooltip = function () {
// Private method
function _createTooltip() {
var tooltip = document.createElement('span');
tooltip.className = 'yla-tooltip';
return tooltip;
}
function _showTooltip(target, tooltip, placement) {
// Put tooltip in document
document.body.appendChild(tooltip);
// Calculate position for tooltip
var placement = placement ? placement + '' : '',
targetRect = target.getBoundingClientRect(),
tooltipRect = tooltip.getBoundingClientRect(),
targetCenterX = targetRect.left + (targetRect.width / 2),
targetCenterY = targetRect.top + (targetRect.height / 2),
className, tooltipX, tooltipY;
switch (placement.toLowerCase()) {
case 'top':
className = 'top';
tooltipX = targetCenterX - (tooltipRect.width / 2);
tooltipY = targetRect.top - tooltipRect.height;
break;
case 'bottom':
className = 'bottom';
tooltipX = targetCenterX - (tooltipRect.width / 2);
tooltipY = targetRect.bottom;
break;
case 'left':
className = 'left';
tooltipX = targetRect.left - tooltipRect.width;
tooltipY = targetCenterY - (tooltipRect.height / 2);
break;
case 'right':
default:
className = 'right';
tooltipX = targetRect.right;
tooltipY = targetCenterY - (tooltipRect.height / 2);
break;
}
// Position tooltip
tooltip.style.position = 'fixed';
tooltip.style.top = tooltipY + 'px';
tooltip.style.left = tooltipX + 'px';
tooltip.className = 'yla-tooltip ' + className;
}
function _removeTooltip(tooltip) {
document.body.removeChild(tooltip);
}
return {
props: {
placement: {
type: String,
default: ''
},
content: {
type: String,
default: ''
}
},
data: function () {
return {
tooltip: _createTooltip()
};
},
watch: {
content: {
immediate: true,
handler: function () {
this.tooltip.textContent = this.content;
}
}
},
render: function (createElement) {
// Make sure this component contain at least one element
var nodes = this.$slots.default || [],
mainElement = nodes.find(node => {
return node.tag && node.tag !== '';
});
if (!mainElement) return;
// Set event handler for main element
var newData = mainElement.data || {};
newData.on = newData.on || {};
newData.on.mouseenter = (evt) => {
_showTooltip(evt.target, this.tooltip, this.placement);
};
newData.on.mouseleave = () => {
_removeTooltip(this.tooltip);
};
// Return main element
mainElement.data = newData;
return mainElement;
}
}
};

54
view/js/page/base.js Normal file
View File

@ -0,0 +1,54 @@
var Base = function () {
return {
data() {
return {
dialog: {}
}
},
methods: {
_defaultDialog() {
return {
visible: false,
loading: false,
title: '',
content: '',
fields: [],
showLabel: false,
mainText: 'Yes',
secondText: '',
mainClick: () => {
this.dialog.visible = false;
},
secondClick: () => {
this.dialog.visible = false;
}
}
},
showDialog(cfg) {
var base = this._defaultDialog();
base.visible = true;
if (cfg.loading) base.loading = cfg.loading;
if (cfg.title) base.title = cfg.title;
if (cfg.content) base.content = cfg.content;
if (cfg.fields) base.fields = cfg.fields;
if (cfg.showLabel) base.showLabel = cfg.showLabel;
if (cfg.mainText) base.mainText = cfg.mainText;
if (cfg.secondText) base.secondText = cfg.secondText;
if (cfg.mainClick) base.mainClick = cfg.mainClick;
if (cfg.secondClick) base.secondClick = cfg.secondClick;
this.dialog = base;
},
showErrorDialog(msg) {
this.showDialog({
visible: true,
title: 'Error',
content: msg,
mainText: 'OK',
mainClick: () => {
this.dialog.visible = false;
}
});
}
}
}
};

View File

@ -1,257 +0,0 @@
! function (e, t) {
"function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? module.exports = t() : e.salvattore = t()
}(this, function () {
window.matchMedia || (window.matchMedia = function () {
"use strict";
var e = window.styleMedia || window.media;
if (!e) {
var t = document.createElement("style"),
n = document.getElementsByTagName("script")[0],
r = null;
t.type = "text/css", t.id = "matchmediajs-test", n.parentNode.insertBefore(t, n), r = "getComputedStyle" in window && window.getComputedStyle(t, null) || t.currentStyle, e = {
matchMedium: function (e) {
var n = "@media " + e + "{ #matchmediajs-test { width: 1px; } }";
return t.styleSheet ? t.styleSheet.cssText = n : t.textContent = n, "1px" === r.width
}
}
}
return function (t) {
return {
matches: e.matchMedium(t || "all"),
media: t || "all"
}
}
}()),
function () {
"use strict";
if (window.matchMedia && window.matchMedia("all").addListener) return !1;
var e = window.matchMedia,
t = e("only all").matches,
n = !1,
r = 0,
a = [],
i = function (t) {
clearTimeout(r), r = setTimeout(function () {
for (var t = 0, n = a.length; n > t; t++) {
var r = a[t].mql,
i = a[t].listeners || [],
o = e(r.media).matches;
if (o !== r.matches) {
r.matches = o;
for (var c = 0, l = i.length; l > c; c++) i[c].call(window, r)
}
}
}, 30)
};
window.matchMedia = function (r) {
var o = e(r),
c = [],
l = 0;
return o.addListener = function (e) {
t && (n || (n = !0, window.addEventListener("resize", i, !0)), 0 === l && (l = a.push({
mql: o,
listeners: c
})), c.push(e))
}, o.removeListener = function (e) {
for (var t = 0, n = c.length; n > t; t++) c[t] === e && c.splice(t, 1)
}, o
}
}(),
function () {
"use strict";
for (var e = 0, t = ["ms", "moz", "webkit", "o"], n = 0; n < t.length && !window.requestAnimationFrame; ++n) window.requestAnimationFrame = window[t[n] + "RequestAnimationFrame"], window.cancelAnimationFrame = window[t[n] + "CancelAnimationFrame"] || window[t[n] + "CancelRequestAnimationFrame"];
window.requestAnimationFrame || (window.requestAnimationFrame = function (t, n) {
var r = (new Date).getTime(),
a = Math.max(0, 16 - (r - e)),
i = window.setTimeout(function () {
t(r + a)
}, a);
return e = r + a, i
}), window.cancelAnimationFrame || (window.cancelAnimationFrame = function (e) {
clearTimeout(e)
})
}(), "function" != typeof window.CustomEvent && ! function () {
"use strict";
function e(e, t) {
t = t || {
bubbles: !1,
cancelable: !1,
detail: void 0
};
var n = document.createEvent("CustomEvent");
return n.initCustomEvent(e, t.bubbles, t.cancelable, t.detail), n
}
e.prototype = window.Event.prototype, window.CustomEvent = e
}();
var e = function (e, t, n) {
"use strict";
var r = {},
a = [],
i = [],
o = [],
c = function (e, t, n) {
e.dataset ? e.dataset[t] = n : e.setAttribute("data-" + t, n)
};
return r.obtainGridSettings = function (t) {
var n = e.getComputedStyle(t, ":before"),
r = n.getPropertyValue("content").slice(1, -1),
a = r.match(/^\s*(\d+)(?:\s?\.(.+))?\s*$/),
i = 1,
o = [];
return a ? (i = a[1], o = a[2], o = o ? o.split(".") : ["column"]) : (a = r.match(/^\s*\.(.+)\s+(\d+)\s*$/), a && (o = a[1], i = a[2], i && (i = i.split(".")))), {
numberOfColumns: i,
columnClasses: o
}
}, r.addColumns = function (e, n) {
for (var a, i = r.obtainGridSettings(e), o = i.numberOfColumns, l = i.columnClasses, s = new Array(+o), u = t.createDocumentFragment(), d = o; 0 !== d--;) a = "[data-columns] > *:nth-child(" + o + "n-" + d + ")", s.push(n.querySelectorAll(a));
s.forEach(function (e) {
var n = t.createElement("div"),
r = t.createDocumentFragment();
n.className = l.join(" "), Array.prototype.forEach.call(e, function (e) {
r.appendChild(e)
}), n.appendChild(r), u.appendChild(n)
}), e.appendChild(u), c(e, "columns", o)
}, r.removeColumns = function (n) {
var r = t.createRange();
r.selectNodeContents(n);
var a = Array.prototype.filter.call(r.extractContents().childNodes, function (t) {
return t instanceof e.HTMLElement
}),
i = a.length,
o = a[0].childNodes.length,
l = new Array(o * i);
Array.prototype.forEach.call(a, function (e, t) {
Array.prototype.forEach.call(e.children, function (e, n) {
l[n * i + t] = e
})
});
var s = t.createElement("div");
return c(s, "columns", 0), l.filter(function (e) {
return !!e
}).forEach(function (e) {
s.appendChild(e)
}), s
}, r.recreateColumns = function (t) {
e.requestAnimationFrame(function () {
r.addColumns(t, r.removeColumns(t));
var e = new CustomEvent("columnsChange");
t.dispatchEvent(e)
})
}, r.mediaQueryChange = function (e) {
e.matches && Array.prototype.forEach.call(a, r.recreateColumns)
}, r.getCSSRules = function (e) {
var t;
try {
t = e.sheet.cssRules || e.sheet.rules
} catch (e) {
return []
}
return t || []
}, r.getStylesheets = function () {
var e = Array.prototype.slice.call(t.querySelectorAll("style"));
return e.forEach(function (t, n) {
"text/css" !== t.type && "" !== t.type && e.splice(n, 1)
}), Array.prototype.concat.call(e, Array.prototype.slice.call(t.querySelectorAll("link[rel='stylesheet']")))
}, r.mediaRuleHasColumnsSelector = function (e) {
var t, n;
try {
t = e.length
} catch (e) {
t = 0
}
for (; t--;)
if (n = e[t], n.selectorText && n.selectorText.match(/\[data-columns\](.*)::?before$/)) return !0;
return !1
}, r.scanMediaQueries = function () {
var t = [];
if (e.matchMedia) {
r.getStylesheets().forEach(function (e) {
Array.prototype.forEach.call(r.getCSSRules(e), function (e) {
try {
e.media && e.cssRules && r.mediaRuleHasColumnsSelector(e.cssRules) && t.push(e)
} catch (e) {}
})
});
var n = i.filter(function (e) {
return -1 === t.indexOf(e)
});
o.filter(function (e) {
return -1 !== n.indexOf(e.rule)
}).forEach(function (e) {
e.mql.removeListener(r.mediaQueryChange)
}), o = o.filter(function (e) {
return -1 === n.indexOf(e.rule)
}), t.filter(function (e) {
return -1 == i.indexOf(e)
}).forEach(function (t) {
var n = e.matchMedia(t.media.mediaText);
n.addListener(r.mediaQueryChange), o.push({
rule: t,
mql: n
})
}), i.length = 0, i = t
}
}, r.rescanMediaQueries = function () {
r.scanMediaQueries(), Array.prototype.forEach.call(a, r.recreateColumns)
}, r.nextElementColumnIndex = function (e, t) {
var n, r, a, i = e.children,
o = i.length,
c = 0,
l = 0;
for (a = 0; o > a; a++) n = i[a], r = n.children.length + (t[a].children || t[a].childNodes).length, 0 === c && (c = r), c > r && (l = a, c = r);
return l
}, r.createFragmentsList = function (e) {
for (var n = new Array(e), r = 0; r !== e;) n[r] = t.createDocumentFragment(), r++;
return n
}, r.appendElements = function (e, t) {
var n = e.children,
a = n.length,
i = r.createFragmentsList(a);
Array.prototype.forEach.call(t, function (t) {
var n = r.nextElementColumnIndex(e, i);
i[n].appendChild(t)
}), Array.prototype.forEach.call(n, function (e, t) {
e.appendChild(i[t])
})
}, r.prependElements = function (e, n) {
var a = e.children,
i = a.length,
o = r.createFragmentsList(i),
c = i - 1;
n.forEach(function (e) {
var t = o[c];
t.insertBefore(e, t.firstChild), 0 === c ? c = i - 1 : c--
}), Array.prototype.forEach.call(a, function (e, t) {
e.insertBefore(o[t], e.firstChild)
});
for (var l = t.createDocumentFragment(), s = n.length % i; 0 !== s--;) l.appendChild(e.lastChild);
e.insertBefore(l, e.firstChild)
}, r.registerGrid = function (n) {
if ("none" !== e.getComputedStyle(n).display) {
var i = t.createRange();
i.selectNodeContents(n);
var o = t.createElement("div");
o.appendChild(i.extractContents()), c(o, "columns", 0), r.addColumns(n, o), a.push(n)
}
}, r.init = function () {
var e = t.createElement("style");
e.innerHTML = "[data-columns]::before{display:block;visibility:hidden;position:absolute;font-size:1px;}", t.head.appendChild(e);
var n = t.querySelectorAll("[data-columns]");
Array.prototype.forEach.call(n, r.registerGrid), r.scanMediaQueries()
}, r.init(), {
appendElements: r.appendElements,
prependElements: r.prependElements,
registerGrid: r.registerGrid,
recreateColumns: r.recreateColumns,
rescanMediaQueries: r.rescanMediaQueries,
init: r.init,
append_elements: r.appendElements,
prepend_elements: r.prependElements,
register_grid: r.registerGrid,
recreate_columns: r.recreateColumns,
rescan_media_queries: r.rescanMediaQueries
}
}(window, window.document);
return e
});

View File

@ -9,6 +9,10 @@
hyphens: auto;
}
a {
cursor: pointer;
}
.spacer {
flex: 1 0;
}
@ -22,7 +26,7 @@
flex-flow: column nowrap;
align-items: center;
height: 100vh;
background-color: @appBg;
background-color: @bg;
justify-content: center;
>.error-message {
width: 100%;
@ -68,13 +72,13 @@
align-items: baseline;
padding: 8px;
p {
color: @fontLightColor;
color: @colorLight;
font-size: 0.9em;
margin-right: 16px;
min-width: 65px;
}
input {
color: @fontColor;
color: @color;
padding: 8px;
border: 1px solid @border;
flex: 1 0;
@ -83,13 +87,13 @@
a {
display: block;
cursor: pointer;
color: @fontLightColor;
color: @colorLight;
text-align: center;
font-size: 0.9em;
flex: 1 0;
i {
margin-right: 8px;
color: @fontLightColor;
color: @colorLight;
}
&:hover {
color: @main;
@ -102,9 +106,9 @@
flex-flow: row nowrap;
padding: 16px;
a {
color: @linkColor;
color: @colorLink;
text-transform: uppercase;
background-color: @headerInputBg;
background-color: @contentBg;
flex: 1 0;
text-align: center;
&.button {
@ -118,232 +122,174 @@
}
}
#main-page {
background-color: @appBg;
#index-page {
display: flex;
flex-flow: column nowrap;
height: auto;
min-height: 100vh;
#header {
background-color: @contentBg;
box-shadow: 0 0 3px @headerShadow;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 99;
flex-flow: row nowrap;
width: 100vw;
height: 100vh;
background-color: @bg;
min-width: 0;
min-height: 0;
overflow: hidden;
#sidebar {
display: flex;
flex-flow: row wrap;
#n-selected {
line-height: @headerHeight;
font-size: 1.3em;
color: @fontLightColor;
flex: 1 0;
border-right: 1px solid @border;
padding: 0 32px;
}
flex-flow: column nowrap;
flex-shrink: 0;
background-color: @darkBg;
min-width: 0;
min-height: 0;
overflow: hidden;
#logo {
border-left: 1px solid @border;
cursor: default;
flex-shrink: 0;
.header-link;
line-height: @headerHeight;
display: flex;
flex-flow: row nowrap;
font-size: 1.5em;
font-weight: 100;
color: @main;
span {
margin-right: 8px;
}
width: 60px;
line-height: 60px;
text-align: center;
font-size: 2em;
color: @contentBg;
background-color: @main;
}
>a {
width: 60px;
line-height: 60px;
text-align: center;
display: block;
color: @colorLight;
&:hover {
background-color: @appBg;
background-color: @color;
}
}
#search-box {
align-items: center;
border-right: 1px solid @border;
display: flex;
flex: 1 0;
flex-flow: row nowrap;
padding: 16px;
width: 100%;
.button,
input {
background-color: @headerInputBg;
border: 1px solid @border;
color: @fontColor;
font-size: 0.9em;
padding: 8px;
}
.button {
cursor: pointer;
color: @linkColor;
&:hover {
color: @accent;
}
}
input {
border-right: 0;
flex: 1 0;
padding: 8px 16px;
min-width: 0;
width: 100%;
}
}
#header-menu {
display: flex;
flex-flow: row nowrap;
a {
line-height: @headerHeight;
padding: 0 16px;
color: @linkColor;
font-size: 0.9em;
cursor: pointer;
&:not(:last-child) {
border-right: 1px solid @border;
}
span {
margin-left: 4px;
}
&:hover {
color: @main;
background-color: @appBg;
}
&.active {
background-color: @color;
color: @main;
}
}
}
#main {
margin-top: @headerHeight;
#body {
flex: 1 0;
display: flex;
flex-flow: column nowrap;
#input-bookmark {
align-self: center;
max-width: 600px;
width: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
#header {
background-color: @contentBg;
border-bottom: 1px solid @border;
display: flex;
flex-flow: column nowrap;
margin: 32px 16px 20px;
background-color: @headerInputBg;
outline: 1px solid @border;
>p {
color: @fontColor;
font-weight: 600;
text-transform: uppercase;
padding: 16px;
&.error-message {
color: @main;
font-size: 0.9em;
border-bottom: 1px solid @border;
font-weight: 500;
text-transform: none;
}
flex-flow: row nowrap;
color: @color;
align-items: center;
min-width: 0;
min-height: 0;
overflow: hidden;
flex-shrink: 0;
input {
flex: 1 0;
line-height: 60px;
padding: 0 16px;
font-size: 1em;
border-right: 1px solid @border;
}
input[type=text],
textarea {
outline: 1px solid @border;
color: @fontColor;
font-size: 0.9em;
padding: 12px 16px;
}
textarea {
resize: vertical;
min-height: 4em;
max-height: 10em;
}
.button-area {
display: flex;
flex-flow: row nowrap;
padding: 8px;
a {
color: @linkColor;
text-transform: uppercase;
padding: 8px;
background-color: @headerInputBg;
font-size: 0.9em;
&.button {
cursor: pointer;
&:hover {
color: @accent;
}
}
}
}
}
#search-parameter {
display: flex;
flex-flow: row wrap;
padding: 0 8px;
a {
display: block;
margin: 8px;
padding: 8px;
font-size: 0.9em;
background-color: @fontLightColor;
color: white;
border-radius: 16px;
cursor: pointer;
&:hover {
background-color: @main;
text-decoration: line-through;
width: 60px;
line-height: 60px;
text-align: center;
color: @colorLink;
&:hover,
&:focus {
color: @main;
}
}
}
#grid {
display: flex;
flex-flow: row nowrap;
padding: 4px;
>.column {
flex: 1 0;
padding: 12px;
max-width: 100%;
>*:not(:last-child) {
margin-bottom: 24px;
overflow-y: auto;
display: grid;
grid-template-rows: auto;
grid-template-columns: repeat(4, 1fr);
grid-gap: 16px;
padding: 16px;
.bookmark {
display: flex;
flex-flow: column nowrap;
min-width: 0;
border: 1px solid @border;
background-color: @contentBg;
height: 100%;
&:last-child {
margin-bottom: 16px;
}
&:hover {
.bookmark-menu>a {
display: block;
}
}
.bookmark-content {
display: block;
position: relative;
flex: 1;
&:hover,
&:focus {
.title {
color: @main;
}
}
>*:not(:last-child) {
margin-bottom: 8px;
}
img {
max-width: 100%;
}
.title {
font-size: 1.2em;
font-weight: 600;
padding: 0 16px;
color: @color;
&:first-child {
padding-top: 16px;
}
}
.excerpt {
color: @color;
padding: 0 16px;
text-overflow: ellipsis;
word-wrap: break-word;
overflow: hidden;
font-size: 0.9em;
line-height: 1.5em;
max-height: 12em;
}
}
.bookmark-menu {
padding: 8px 16px 16px;
display: flex;
flex-flow: row nowrap;
min-width: 0;
min-height: 0;
align-items: center;
a {
color: @colorLink;
flex-shrink: 0;
opacity: 0.8;
display: none;
font-size: 0.9em;
&:not(:last-child) {
margin-right: 12px;
}
&:hover,
&:focus {
color: @main;
opacity: 1;
}
}
.url {
flex: 1 0;
opacity: 1;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 21px;
}
}
}
}
#message-bar {
display: flex;
flex-flow: column;
align-items: center;
padding: 32px;
justify-content: center;
position: absolute;
top: 50%;
left: 0;
width: 100%;
margin-top: -60px;
height: 120px;
i {
color: @fontLightColor;
font-size: 3em;
}
}
}
@media screen and (max-width: 800px) {
#header {
position: static;
#header-menu>a>span {
display: none;
}
}
#main {
margin-top: 0;
.bookmark-menu>a>span {
display: none;
}
}
}
@media screen and (max-width: 740px) {
#main #input-bookmark {
width: auto;
align-self: auto;
}
}
@media screen and (max-width: 500px) {
#header #logo {
display: none;
}
}
}
@ -375,7 +321,7 @@
flex: 1 0;
font-size: 0.9em;
text-align: center;
color: @linkColor;
color: @colorLink;
padding: 16px;
i {
margin-right: 4px;
@ -385,7 +331,7 @@
border-right: 1px solid @border;
}
&:visited {
color: @linkColor
color: @colorLink
}
&:hover {
color: @main;
@ -406,7 +352,7 @@
}
p {
font-size: 0.9em;
color: @fontColor;
color: @color;
}
}
#content {
@ -438,263 +384,4 @@
color: white;
}
}
}
#dialog-overlay {
.full-overlay();
#dialog {
display: flex;
background-color: @contentBg;
align-self: center;
flex-flow: column;
border: 1px solid @border;
max-width: 500px;
#dialog-title {
color: @fontColor;
font-weight: 600;
text-transform: uppercase;
padding: 16px;
font-size: 1em;
border-bottom: 1px solid @border;
}
#dialog-content {
padding: 16px;
}
#dialog-button {
display: flex;
flex-flow: row nowrap;
padding: 8px;
border-top: 1px solid @border;
a {
color: @linkColor;
text-transform: uppercase;
padding: 8px;
background-color: @headerInputBg;
&.button {
cursor: pointer;
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: @accent;
}
}
}
}
}
}
#tag-cloud-overlay {
.full-overlay();
#tag-cloud {
display: flex;
background-color: @contentBg;
flex-flow: column nowrap;
border: 1px solid @border;
max-height: 100%;
#tag-cloud-title {
display: flex;
flex-flow: row nowrap;
padding: 16px;
border-bottom: 1px solid @border;
align-items: center;
p {
color: @fontColor;
font-weight: 600;
text-transform: uppercase;
font-size: 1em;
flex: 1 0;
}
a {
color: @fontLightColor;
cursor: pointer;
&:hover {
color: @main;
}
}
}
#tag-cloud-content {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
overflow-y: auto;
padding: 12px;
a {
color: @fontLightColor;
cursor: pointer;
margin: 4px;
&:hover {
color: @main;
}
}
}
}
}
.error-message {
color: @main !important;
font-size: 0.9em;
&::before {
content: "\f071";
font-weight: 900;
margin-right: 8px;
font-family: "Font Awesome 5 Free"
}
}
.bookmark {
background-color: @contentBg;
border: 1px solid @border;
position: relative;
.checkbox {
z-index: 9;
right: 0;
opacity: 0;
position: absolute;
outline: 1px solid @border;
color: @linkColor;
background-color: @contentBg;
width: 32px;
line-height: 32px;
text-align: center;
display: block;
cursor: pointer;
font-size: 0.9em;
&:hover {
color: @accent !important;
}
}
.bookmark-metadata {
padding: 16px;
display: flex;
flex-flow: column nowrap;
border-bottom: 1px solid @border;
.bookmark-time {
color: @fontLightColor;
font-size: 0.9em;
margin-bottom: 8px;
}
.bookmark-title {
color: @fontColor;
font-size: 1.3em;
font-weight: 600;
text-overflow: ellipsis;
overflow: hidden;
}
.bookmark-url {
.bookmark-time;
margin-bottom: 0;
margin-top: 8px;
max-height: 2.6em;
line-height: 1.3em;
text-overflow: ellipsis;
overflow: hidden;
}
&.has-image {
min-height: 250px;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
justify-content: flex-end;
position: relative;
&::before {
content: "";
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
.bookmark-time,
.bookmark-url {
z-index: 2;
color: white;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
.bookmark-title {
z-index: 2;
color: white;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
}
&:hover {
.bookmark-title {
text-decoration: underline;
}
}
}
.bookmark-excerpt {
padding: 16px 16px 0;
color: @fontColor;
text-overflow: ellipsis;
overflow: hidden;
}
.bookmark-tags {
display: flex;
flex-flow: row wrap;
padding: 12px 12px 0;
margin-bottom: -4px;
a {
cursor: pointer;
font-size: 0.9em;
padding: 4px;
color: @main !important;
&::before {
content: "#"
}
&:hover {
text-decoration: underline;
}
}
}
.bookmark-menu {
display: flex;
flex-flow: row nowrap;
border-top: 1px solid @border;
visibility: hidden;
margin-top: 16px;
&:nth-child(3) {
border-top: 0;
margin-top: 0;
}
a {
cursor: pointer;
display: block;
flex: 1 0;
color: @linkColor !important;
padding: 8px;
font-size: 0.9em;
text-align: center;
span {
margin-left: 4px;
}
&:not(:last-child) {
border-right: 1px solid @border;
}
&:hover {
color: @accent !important;
}
}
}
&:hover {
.checkbox {
opacity: 1;
}
.bookmark-menu {
visibility: visible;
}
}
&.checked {
border: 1px solid @borderDark;
outline: 6px solid @borderDark;
.checkbox {
opacity: 1;
outline: 0;
background-color: @borderDark;
color: white;
}
}
}

View File

@ -1,21 +1,36 @@
// out: false
@appBg: #F5F5F5;
//
// Background
@bg: #EEE;
@darkBg: #353535;
@contentBg: #FFF;
//
// Border
@border: #E5E5E5;
@borderDark: #9E9E9E;
@contentBg: #FFF;
@headerInputBg: #FFF;
@fontColor: #000;
@linkColor: #535A60;
@fontLightColor: #6F757A;
//
// Font color
@color: #232323;
@colorLink: #999;
@colorLight: #EEE;
@fontSize: 0.9em;
@headerHeight: 70px;
//
// Color theme
@main: #F44336;
@accent: #F44336;
@headerShadow: rgba(0, 0, 0, 0.3);
@mainDark: #B71C1C;
@accent: #f4a236;
//
// Tooltip
@tooltipBg: #232323;
@arrowWidth: 8px;
//
// Other variable
@headerHeight: 60px;
//
// Mixin
.header-link {
border-right: 1px solid @border;
color: @fontColor;
color: @color;
cursor: pointer;
font-size: @fontSize;
line-height: @headerHeight;

101
view/less/yla-dialog.less Normal file
View File

@ -0,0 +1,101 @@
@import "variable";
.yla-dialog__overlay {
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10001;
background-color: rgba(0, 0, 0, 0.6);
padding: 32px;
.yla-dialog {
display: flex;
flex-flow: column nowrap;
min-width: 400px;
min-height: 0;
max-height: 100%;
overflow: hidden;
background-color: #FFF;
font-size: 16px;
>.yla-dialog__header {
display: flex;
flex-flow: row nowrap;
padding: 16px;
min-height: 0;
background-color: @darkBg;
color: @colorLight;
flex-shrink: 0;
>p {
flex: 1 0;
font-weight: 600;
font-size: 1em;
text-transform: uppercase;
}
>a:hover {
color: @main;
}
}
>.yla-dialog__body {
padding: 16px;
display: grid;
max-height: 100%;
min-height: 80px;
min-width: 0;
font-size: 0.9em;
overflow: auto;
grid-template-columns: max-content 1fr;
align-items: baseline;
grid-gap: 16px;
>.yla-dialog__content {
grid-column-start: 1;
grid-column-end: 3;
align-self: baseline;
}
>input {
color: @color;
padding: 8px;
border: 1px solid @border;
}
>.suggestion {
position: absolute;
display: block;
padding: 8px;
background-color: @bg;
border: 1px solid @border;
}
}
>.yla-dialog__footer {
padding: 16px;
display: flex;
flex-flow: row wrap;
justify-content: flex-end;
border-top: 1px solid @border;
>a {
text-transform: uppercase;
padding: 0 8px;
font-size: 0.9em;
font-weight: 600;
&:hover {
color: @main;
}
&:focus {
outline: none;
color: @main;
border-bottom: 1px dashed @main;
}
}
>i {
width: 19px;
line-height: 19px;
text-align: center;
}
}
}
}

View File

@ -0,0 +1,52 @@
@import "variable";
.yla-tooltip {
font-size: 14px;
color: @colorLight;
background-color: @tooltipBg;
padding: 8px;
border-radius: 4px;
position: relative;
z-index: 1000;
&::after {
content: '';
display: block;
position: absolute;
border: @arrowWidth solid transparent;
}
&.left {
margin-left: -(@arrowWidth*2-2);
&::after {
top: 50%;
right: -2*@arrowWidth;
margin-top: -@arrowWidth;
border-left-color: @tooltipBg
}
}
&.top {
margin-top: -(@arrowWidth*2-2);
&::after {
left: 50%;
bottom: -2*@arrowWidth;
margin-left: -@arrowWidth;
border-top-color: @tooltipBg
}
}
&.right {
margin-left: @arrowWidth*2-2;
&::after {
top: 50%;
left: -2*@arrowWidth;
margin-top: -@arrowWidth;
border-right-color: @tooltipBg
}
}
&.bottom {
margin-top: @arrowWidth*2-2;
&::after {
left: 50%;
top: -2*@arrowWidth;
margin-left: -@arrowWidth;
border-bottom-color: @tooltipBg
}
}
}