1
0
mirror of https://github.com/Kovah/LinkAce.git synced 2025-02-24 11:13:02 +01:00

Merge pull request #48 from Kovah/dev

v0.0.14
This commit is contained in:
Kevin Woblick 2019-04-27 12:09:02 +02:00 committed by GitHub
commit 9cb4f25e4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 267 additions and 73 deletions

5
.babelrc Normal file
View File

@ -0,0 +1,5 @@
{
"presets": [
["env"]
]
}

View File

@ -231,6 +231,15 @@ Currently you can do this by using the command line:
docker exec -it linkace-php bash -c "php artisan registeruser [user name] [user email]"
```
### Tests
You may run some existing tests with the following command:
```bash
docker exec -it linkace-php bash -c "./vendor/bin/phpunit"
```
---
LinkAce is a project by [Kovah](https://kovah.de) | [Contributors](https://github.com/Kovah/LinkAce/graphs/contributors)

View File

@ -17,10 +17,8 @@ class LinkAce
*/
public static function getMetaFromURL(string $url)
{
$title_fallback = parse_url($url, PHP_URL_HOST);
$fallback = [
'title' => $title_fallback,
'title' => parse_url($url, PHP_URL_HOST),
'description' => null,
];
@ -31,7 +29,14 @@ class LinkAce
return $fallback;
}
if (!$html) {
// Try to get the meta tags of that URL
try {
$tags = get_meta_tags($url);
} catch (\Exception $e) {
return $fallback;
}
if (empty($html)) {
return $fallback;
}
@ -44,22 +49,15 @@ class LinkAce
$title = trim($title);
}
// Parse the HTML for the meta description, or alternatively for the og:description property
$res = preg_match(
'/<meta (?:property="og:description"|name="description") content="(.*?)"(?:\s\/)?>/i',
$html,
$description_matches
);
if ($res) {
// Clean up description: remove EOL's and excessive whitespace.
$description = preg_replace('/\s+/', ' ', $description_matches[1]);
$description = trim($description);
}
// Get the title or the og:description tag or the twitter:description tag
$description = $tags['description']
?? $tags['og:description']
?? $tags['twitter:description']
?? $fallback['description'];
return [
'title' => $title ?? $title_fallback,
'description' => $description ?? null,
'title' => $title,
'description' => $description,
];
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\Link;
use App\Models\Tag;
use Illuminate\Http\Request;
@ -43,4 +44,30 @@ class AjaxController extends Controller
return response()->json($tags);
}
/**
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function searchExistingUrls(Request $request)
{
$query = $request->get('query', false);
if (!$query) {
return response()->json([]);
}
// Search for tags
$links = Link::byUser(auth()->user()->id)
->where('url', trim($query))
->first();
if (empty($links)) {
// No links found
return response()->json(['linkFound' => false]);
}
// Link found
return response()->json(['linkFound' => true]);
}
}

View File

@ -36,7 +36,7 @@ services:
container_name: "linkace-nginx"
image: bitnami/nginx:1.14
ports:
- "127.0.0.1:80:8085"
- "80:8085"
depends_on:
- php
volumes:

63
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "linkace",
"version": "0.0.13",
"version": "0.0.14",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -72,9 +72,9 @@
"dev": true
},
"ajv": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
"integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
"dev": true,
"requires": {
"fast-deep-equal": "^2.0.1",
@ -4879,9 +4879,9 @@
"integrity": "sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ=="
},
"js-base64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz",
"integrity": "sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
"integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
"dev": true
},
"js-tokens": {
@ -5116,18 +5116,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
"dev": true
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.isfinite": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz",
@ -5146,12 +5134,6 @@
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==",
"dev": true
},
"lodash.mergewith": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
"integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
"dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -5471,7 +5453,8 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
"integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==",
"dev": true
"dev": true,
"optional": true
},
"nanomatch": {
"version": "1.2.13",
@ -5559,9 +5542,9 @@
}
},
"node-sass": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz",
"integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
"integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@ -5571,12 +5554,10 @@
"get-stdin": "^4.0.1",
"glob": "^7.0.3",
"in-publish": "^2.0.0",
"lodash.assign": "^4.2.0",
"lodash.clonedeep": "^4.3.2",
"lodash.mergewith": "^4.6.0",
"lodash": "^4.17.11",
"meow": "^3.7.0",
"mkdirp": "^0.5.1",
"nan": "^2.10.0",
"nan": "^2.13.2",
"node-gyp": "^3.8.0",
"npmlog": "^4.0.0",
"request": "^2.88.0",
@ -5614,6 +5595,12 @@
"which": "^1.2.9"
}
},
"nan": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
"dev": true
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@ -7295,9 +7282,9 @@
"dev": true
},
"sshpk": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
"integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
"integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
"dev": true,
"requires": {
"asn1": "~0.2.3",
@ -7558,7 +7545,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@ -8189,7 +8176,7 @@
},
"yargs": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz",
"resolved": "http://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz",
"integrity": "sha1-gW4ahm1VmMzzTlWW3c4i2S2kkNQ=",
"dev": true,
"requires": {

View File

@ -1,6 +1,6 @@
{
"name": "linkace",
"version": "0.0.13",
"version": "0.0.14",
"description": "A small, selfhosted bookmark manager with advanced features, built with Laravel and Docker",
"homepage": "https://github.com/Kovah/LinkAce",
"repository": {
@ -34,7 +34,7 @@
"grunt-postcss": "^0.9.0",
"grunt-sass": "^3.0.2",
"load-grunt-tasks": "^4.0.0",
"node-sass": "^4.11.0",
"node-sass": "^4.12.0",
"postcss": "^6.0.23",
"time-grunt": "^1.4.0",
"uglifyify": "^5.0.1"

View File

@ -1,12 +1,20 @@
// Set CSS based on
const preferDarkmode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const darkmodeAuto = document.documentElement.querySelector('meta[name="darkmode"]');
import { register } from './lib/views';
if (darkmodeAuto) {
const stylesheet = document.documentElement.querySelector('[rel="stylesheet"]');
if (preferDarkmode) {
stylesheet.href = stylesheet.dataset.darkHref;
} else {
stylesheet.href = stylesheet.dataset.lightHref;
}
// Register components
import Base from './components/Base';
import UrlField from './components/UrlField';
// Register view components
function registerViews () {
// register component views
register('#app', Base);
register('input[id="url"]', UrlField);
}
if (document.readyState !== 'loading') {
// dom loaded event already fired
registerViews();
} else {
// wait for the dom to load
document.addEventListener('DOMContentLoaded', registerViews);
}

28
resources/assets/js/components/Base.js vendored Normal file
View File

@ -0,0 +1,28 @@
export default class Base {
constructor ($el) {
this.initAppData();
this.initAutoDarkmode();
}
initAppData () {
// Load data passed by the backend to the JS
let data = document.querySelector('meta[property="la-app-data"]').getAttribute('content');
window.appData = JSON.parse(data);
}
initAutoDarkmode () {
// Set CSS based on user preference for dark mode
const preferDarkmode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const darkmodeAuto = document.querySelector('meta[name="darkmode"]');
if (darkmodeAuto) {
const stylesheet = document.querySelector('[rel="stylesheet"]');
if (preferDarkmode) {
stylesheet.href = stylesheet.dataset.darkHref;
} else {
stylesheet.href = stylesheet.dataset.lightHref;
}
}
}
}

View File

@ -0,0 +1,53 @@
import { debounce } from '../lib/helper';
export default class UrlField {
constructor ($el) {
this.$field = $el;
this.$field.addEventListener('keyup', this.onKeyup.bind(this));
}
onKeyup () {
// Debounce the keyup function to wait 500ms until the last input was typed
debounce(() => {
const value = this.$field.value;
// Check for existing links if the value is longer than http://
if (value.length > 6) {
this.checkforExistingUrl(value);
} else {
this.resetField();
}
});
}
checkforExistingUrl (url) {
const checkUrl = window.appData.routes.ajax.existingLinks;
fetch(checkUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
_token: window.appData.user.token,
query: url
})
}).then((response) => {
return response.json();
}).then((result) => {
// If the link already exist, mark the field as invalid
if (result.linkFound === true) {
this.$field.classList.add('is-invalid');
} else {
this.$field.classList.remove('is-invalid');
}
});
}
resetField () {
this.$field.classList.remove('is-invalid');
}
}

16
resources/assets/js/lib/helper.js vendored Normal file
View File

@ -0,0 +1,16 @@
/**
* Debounce a function with a given timeout
*
* @see https://gist.github.com/makenova/7885923
* @param {CallableFunction} callback
* @param {int} [timeout=500] timeout
*/
export function debounce (callback, timeout = 500) {
if (window.timeoutId) {
window.clearTimeout(window.timeoutId);
}
window.timeoutId = window.setTimeout(function () {
callback();
}, timeout);
}

46
resources/assets/js/lib/views.js vendored Normal file
View File

@ -0,0 +1,46 @@
/**
* View elements
* @type {HTMLElement[]}
*/
const $views = [];
/**
* View instances
* @type {object[]}
*/
const views = [];
/**
* Registers a view to a name and saves a reference.
* @param {string} name View name
* @param {function} invokable View class
* @param {HTMLElement} [$root=document.documentElement] Root element
* @return {void}
*/
export function register (name, invokable, $root = document.documentElement) {
// Retrieve all view elements in root
const $elements = [...$root.querySelectorAll(`${name}`)];
// Create an instance for each view
$elements.forEach($element => getInstance($element, invokable));
}
/**
* Returns the view instance by given element.
* @return {HTMLElement} $element Element
* @return {function} invokable Element class
* @return {object}|null View instance or null if there is none
*/
export function getInstance ($element, invokable = null) {
const index = $views.indexOf($element);
if (index !== -1) {
return views[index];
} else if (invokable !== null && $views.indexOf($element) === -1) {
const view = new invokable($element);
$views.push($element);
views.push(view);
return view;
} else {
return null;
}
}

View File

@ -10,6 +10,7 @@ body:not(.bookmarklet) {
.footer {
opacity: .5;
padding-bottom: ($spacer * 2);
}
@media (max-width: 991px) {

View File

@ -13,6 +13,11 @@
class="form-control form-control-lg{{ $errors->has('url') ? ' is-invalid' : '' }}"
placeholder="@lang('link.url')" value="{{ old('url') ?: $bookmark_url ?? '' }}"
required autofocus>
<p class="invalid-feedback {{ $errors->has('url') ? 'd-none' : '' }}">
@lang('validation.unique', ['attribute' => trans('link.url')])
</p>
@if ($errors->has('url'))
<p class="invalid-feedback" role="alert">
{{ $errors->first('url') }}
@ -57,7 +62,7 @@
class="{{ $errors->has('category_id') ? ' is-invalid' : '' }}">
<option value="0">@lang('category.select_category')</option>
@foreach($categories as $category)
@if($category->childCategories)
@if($category->childCategories->count() > 0)
<optgroup label="{{ $category->name }}">
<option value="{{ $category->id }}">
{{ $category->name }}
@ -77,7 +82,6 @@
@endforeach
</select>
@if ($errors->has('category_id'))
<p class="invalid-feedback" role="alert">
{{ $errors->first('category_id') }}

View File

@ -32,7 +32,7 @@
</div>
<button type="submit" class="btn btn-sm btn-primary">
<i class="fa fa-save fa-mr"></i> @lang('link.add')
<i class="fa fa-save fa-mr"></i> @lang('note.add')
</button>
</div>

View File

@ -6,7 +6,7 @@
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
<ul class="mb-0 list-unstyled">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach

View File

@ -20,6 +20,17 @@
<link href="{{ asset('assets/dist/css/fa.min.css') }}" rel="stylesheet">
<meta property="la-app-data" content="{{ json_encode([
'user' => [
'token' => csrf_token()
],
'routes' => [
'ajax' => [
'existingLinks' => route('ajax-existing-links')
]
]
]) }}">
<link rel="apple-touch-icon" sizes="57x57" href="{{ asset('assets/img/apple-icon-57x57.png') }}">
<link rel="apple-touch-icon" sizes="60x60" href="{{ asset('assets/img/apple-icon-60x60.png') }}">
<link rel="apple-touch-icon" sizes="72x72" href="{{ asset('assets/img/apple-icon-72x72.png') }}">

View File

@ -73,6 +73,7 @@ Route::group(['middleware' => ['auth']], function () {
'App\SystemSettingsController@generateCronToken')->name('generate-cron-token');
Route::post('ajax/tags', 'API\AjaxController@getTags')->name('ajax-tags');
Route::post('ajax/existing-links', 'API\AjaxController@searchExistingUrls')->name('ajax-existing-links');
});
// Guest access routes