From d10ed79f01c6595710d9752f80c99e2cec000f6a Mon Sep 17 00:00:00 2001 From: Kovah Date: Wed, 18 Sep 2019 16:59:10 +0200 Subject: [PATCH 1/8] Correct highlighted items in category / tag dropdowns --- .../assets/sass/third-party/selectize/_variables-dark.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/assets/sass/third-party/selectize/_variables-dark.scss b/resources/assets/sass/third-party/selectize/_variables-dark.scss index 80015f37..e17a9bae 100644 --- a/resources/assets/sass/third-party/selectize/_variables-dark.scss +++ b/resources/assets/sass/third-party/selectize/_variables-dark.scss @@ -4,12 +4,12 @@ $selectize-font-size: inherit !default; $selectize-line-height: $input-btn-line-height !default; //formerly line-height-computed $selectize-color-text: $input-color !default; //$gray-800 -$selectize-color-highlight: $gray-400 !default; +$selectize-color-highlight: lighten($primary, 10%) !default; $selectize-color-input: $input-bg !default; $selectize-color-input-full: $input-bg !default; $selectize-color-input-error: theme-color("danger") !default; $selectize-color-input-error-focus: darken($selectize-color-input-error, 10%) !default; -$selectize-color-disabled: $input-bg !default; +$selectize-color-disabled: $gray-800 !default; $selectize-color-item: $gray-800 !default; $selectize-color-item-border: $gray-900 !default; $selectize-color-item-active: $component-active-bg !default; From 63fb816bd5bf757dcb8655e642dadb6a0019d253 Mon Sep 17 00:00:00 2001 From: Kovah Date: Wed, 18 Sep 2019 17:06:10 +0200 Subject: [PATCH 2/8] Update the README with information about the features and the community forums --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 321fd30c..96bc60ac 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,33 @@ ## About LinkAce -> @TODO Screenshot(s) +![Preview Screenshot](https://www.linkace.org/images/preview/linkace_dashboard.png) LinkAce is a bookmark manager similar to Shaarli and other tools. I built this tool to have something that fits my actual needs that other bookmark managers couldn't solve, even if most features are almost the same. ### Features -* Bookmark links with automatic title generation +* Bookmark links with automatic title and description generation * Organize bookmarks in categories and tags * A bookmarklet to quickly save links from any browser -* Private or public links so friends or internet stranges can see your collection +* Private or public links, so friends or internet strangers can see your collection * Add notes to links to add thoughts * Advanced search for your bookmarks +* A built-in light and dark color theme * Import existing bookmarks from HTML exports (other methods planned) +* Automated link checks to make sure your bookmarks stay available +* Automated “backups” of your bookmarks via the Waybackmachine +* Implemented support for complete database and app backups to Amazon AWS S3 More features are already planned. Take a look at our [project board](https://github.com/Kovah/LinkAce/projects/1) -for more information. +for more information. + +### Documentation and Community + +For any further information about all the available features and how to install the app can be found on the +[LinkAce Website](https://www.linkace.org/). Additionally, you may visit the [community forums](https://community.linkace.org/) +to share your ideas, talk with other users or find help for specific problems. --- From d32a47dec53bbc431437f7c02caf43e7c05d52c2 Mon Sep 17 00:00:00 2001 From: Kovah Date: Mon, 23 Sep 2019 12:33:41 +0200 Subject: [PATCH 3/8] Update package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd10d619..c1e39405 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5821,7 +5821,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -7364,7 +7364,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { From 81bad43dc82ed441c464044da41491151c0c895d Mon Sep 17 00:00:00 2001 From: Kovah Date: Mon, 14 Oct 2019 16:31:05 +0200 Subject: [PATCH 4/8] Solve issue with not working loading button (#71) --- resources/assets/js/components/LoadingButton.js | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/assets/js/components/LoadingButton.js b/resources/assets/js/components/LoadingButton.js index 773ad432..7b42f17a 100644 --- a/resources/assets/js/components/LoadingButton.js +++ b/resources/assets/js/components/LoadingButton.js @@ -10,6 +10,7 @@ export default class LoadingButton { onClick () { if (this.formIsValid()) { this.$el.disabled = true; + this.$form.submit(); } } From e8fdbebbcdbfb9f5e4312c8b75c854f96ce8b91f Mon Sep 17 00:00:00 2001 From: Kovah Date: Mon, 14 Oct 2019 16:45:58 +0200 Subject: [PATCH 5/8] Optimize display of links to look better --- resources/assets/sass/third-party/bootstrap/_variables.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/assets/sass/third-party/bootstrap/_variables.scss b/resources/assets/sass/third-party/bootstrap/_variables.scss index 097f12e4..6bd5dc3d 100644 --- a/resources/assets/sass/third-party/bootstrap/_variables.scss +++ b/resources/assets/sass/third-party/bootstrap/_variables.scss @@ -173,8 +173,8 @@ $body-color-muted: $gray !default; $link-color: theme-color("primary") !default; $link-decoration: none !default; -$link-hover-color: darken($link-color, 15%) !default; -$link-hover-decoration: underline !default; +$link-hover-color: darken($link-color, 20%) !default; +$link-hover-decoration: none !default; // Darken percentage for links with `.text-*` class (e.g. `.text-success`) $emphasized-link-hover-darken-percentage: 15% !default; From 7a46352f00bfa671ced01116eb0c69644e9f73d6 Mon Sep 17 00:00:00 2001 From: Kovah Date: Tue, 15 Oct 2019 10:56:39 +0200 Subject: [PATCH 6/8] Refactor the script handling, move everything into pre-build scripts, optimize the darkmode loader (#67) --- resources/assets/js/app.js | 14 +++- .../assets/js/components/BookmarkTimer.js | 17 +++++ .../assets/js/components/GenerateApiToken.js | 61 ++++++++++++++++ .../assets/js/components/GenerateCronToken.js | 72 +++++++++++++++++++ .../assets/js/components/LoadingButton.js | 10 +-- .../assets/js/components/ShareToggleAll.js | 15 ++++ .../assets/js/components/SimpleSelect.js | 8 +++ resources/assets/js/components/TagsSelect.js | 59 +++++++++++++++ resources/assets/sass/custom/_app.scss | 2 +- resources/assets/sass/loader.scss | 4 ++ resources/lang/en/settings.php | 2 +- .../actions/bookmarklet/complete.blade.php | 12 ---- .../views/actions/export/export.blade.php | 12 ---- .../views/actions/import/import.blade.php | 12 ---- .../actions/search/partials/tags-js.blade.php | 24 ------- .../views/actions/search/search.blade.php | 8 +-- .../actions/settings/partials/api.blade.php | 42 ++--------- .../settings/partials/app-settings.blade.php | 10 +-- .../settings/partials/sharing.blade.php | 11 +-- .../settings/partials/system/cron.blade.php | 48 ++----------- .../system/general-settings.blade.php | 10 +-- resources/views/layouts/app.blade.php | 7 +- resources/views/layouts/bookmarklet.blade.php | 7 +- resources/views/layouts/guest.blade.php | 7 +- .../views/models/categories/create.blade.php | 10 +-- .../views/models/categories/edit.blade.php | 10 +-- resources/views/models/links/edit.blade.php | 13 +--- .../links/partials/create-form.blade.php | 11 +-- .../models/links/partials/tags-js.blade.php | 30 -------- resources/views/partials/header.blade.php | 13 +++- resources/views/partials/loader.blade.php | 1 + 31 files changed, 289 insertions(+), 273 deletions(-) create mode 100644 resources/assets/js/components/BookmarkTimer.js create mode 100644 resources/assets/js/components/GenerateApiToken.js create mode 100644 resources/assets/js/components/GenerateCronToken.js create mode 100644 resources/assets/js/components/ShareToggleAll.js create mode 100644 resources/assets/js/components/SimpleSelect.js create mode 100644 resources/assets/js/components/TagsSelect.js delete mode 100644 resources/views/actions/search/partials/tags-js.blade.php delete mode 100644 resources/views/models/links/partials/tags-js.blade.php create mode 100644 resources/views/partials/loader.blade.php diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 5d878c95..000313db 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -3,18 +3,28 @@ import { register } from './lib/views'; import Base from './components/Base'; import UrlField from './components/UrlField'; import LoadingButton from './components/LoadingButton'; +import BookmarkTimer from './components/BookmarkTimer'; +import TagsSelect from './components/TagsSelect'; +import SimpleSelect from './components/SimpleSelect'; +import ShareToggleAll from './components/ShareToggleAll'; +import GenerateApiToken from './components/GenerateApiToken'; +import GenerateCronToken from './components/GenerateCronToken'; // Register view components function registerViews () { register('#app', Base); register('input[id="url"]', UrlField); register('button[type="submit"]', LoadingButton); + register('.bm-timer', BookmarkTimer); + register('.tags-select', TagsSelect); + register('.simple-select', SimpleSelect); + register('.share-toggle', ShareToggleAll); + register('.api-token', GenerateApiToken); + register('.cron-token', GenerateCronToken); } if (document.readyState !== 'loading') { - // dom loaded event already fired registerViews(); } else { - // wait for the dom to load document.addEventListener('DOMContentLoaded', registerViews); } diff --git a/resources/assets/js/components/BookmarkTimer.js b/resources/assets/js/components/BookmarkTimer.js new file mode 100644 index 00000000..18e2093d --- /dev/null +++ b/resources/assets/js/components/BookmarkTimer.js @@ -0,0 +1,17 @@ +export default class BookmarkTimer { + + constructor ($el) { + this.$el = $el; + this.init(); + } + + init () { + window.setInterval(function () { + this.$el.text(parseInt(this.$el.text()) - 1); + }, 1000); + + window.setTimeout(function () { + window.close(); + }, 5000); + } +} diff --git a/resources/assets/js/components/GenerateApiToken.js b/resources/assets/js/components/GenerateApiToken.js new file mode 100644 index 00000000..e411a049 --- /dev/null +++ b/resources/assets/js/components/GenerateApiToken.js @@ -0,0 +1,61 @@ +import { debounce } from '../lib/helper'; + +export default class GenerateApiToken { + + constructor ($el) { + this.$el = $el; + + this.$input = $el.querySelector('.api-token-input'); + this.$btn = $el.querySelector('.api-token-generate'); + this.$failureMsg = $el.querySelector('.api-token-generate-failure'); + + this.$btn.addEventListener('click', this.onButtonClick.bind(this)); + } + + onButtonClick () { + this.$btn.disabled = true; + + this.fetchNewToken(); + } + + fetchNewToken () { + + const fetchURL = window.appData.routes.ajax.generateApiToken; + + fetch(fetchURL, { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + _token: window.appData.user.token + }) + }).then((response) => { + return response.json(); + }).then(response => { + this.handleResponse(response); + }).catch(() => { + this.showFailureMsg(); + }); + + } + + handleResponse (response) { + + if (typeof response.new_token !== 'undefined') { + debounce(() => { + this.$input.value = response.new_token; + }, 1000); + + window.setTimeout(() => { + this.$btn.disabled = false; + }, 5000); + } else { + this.showFailureMsg(); + } + + } + + showFailureMsg () { + this.$failureMsg.classList.remove('d-none'); + } +} diff --git a/resources/assets/js/components/GenerateCronToken.js b/resources/assets/js/components/GenerateCronToken.js new file mode 100644 index 00000000..06aabe9b --- /dev/null +++ b/resources/assets/js/components/GenerateCronToken.js @@ -0,0 +1,72 @@ +import { debounce } from '../lib/helper'; + +export default class GenerateCronToken { + + constructor ($el) { + this.$el = $el; + + this.$input = $el.querySelector('.cron-token-input'); + this.$btn = $el.querySelector('.cron-token-generate'); + this.$failureMsg = $el.querySelector('.cron-token-generate-failure'); + this.$cronUrl = $el.querySelector('.cron-token-url'); + + this.$btn.addEventListener('click', this.onButtonClick.bind(this)); + } + + onButtonClick () { + this.$btn.disabled = true; + + this.fetchNewToken(); + } + + fetchNewToken () { + + if (!confirm(this.$el.dataset.confirmMessage)) { + this.$btn.disabled = false; + return; + } + + const fetchURL = window.appData.routes.ajax.generateCronToken; + + fetch(fetchURL, { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + _token: window.appData.user.token + }) + }).then(response => { + return response.json(); + }).then(response => { + this.handleResponse(response); + }).catch(() => { + this.showFailureMsg(); + }); + + } + + handleResponse (response) { + + if (typeof response.new_token !== 'undefined') { + debounce(() => { + this.$input.value = response.new_token; + this.$cronUrl.innerHTML = this.buildCronURl(response.new_token); + }, 1000); + + window.setTimeout(() => { + this.$btn.disabled = false; + }, 5000); + } else { + this.showFailureMsg(); + } + + } + + showFailureMsg () { + this.$failureMsg.classList.remove('d-none'); + } + + buildCronURl (newToken) { + return this.$el.dataset.cronUrlBase + newToken; + } +} diff --git a/resources/assets/js/components/LoadingButton.js b/resources/assets/js/components/LoadingButton.js index 7b42f17a..d690ed0a 100644 --- a/resources/assets/js/components/LoadingButton.js +++ b/resources/assets/js/components/LoadingButton.js @@ -1,20 +1,20 @@ export default class LoadingButton { constructor ($el) { - this.$el = $el; - this.$form = this.$el.form; + this.$btn = $el; + this.$form = this.$btn.form; - this.$el.addEventListener('click', this.onClick.bind(this)) + this.$btn.addEventListener('click', this.onClick.bind(this)); } onClick () { if (this.formIsValid()) { - this.$el.disabled = true; + this.$btn.disabled = true; this.$form.submit(); } } - formIsValid() { + formIsValid () { return this.$form.checkValidity(); } } diff --git a/resources/assets/js/components/ShareToggleAll.js b/resources/assets/js/components/ShareToggleAll.js new file mode 100644 index 00000000..dd1f7475 --- /dev/null +++ b/resources/assets/js/components/ShareToggleAll.js @@ -0,0 +1,15 @@ +export default class ShareToggleAll { + + constructor ($el) { + this.$toggle = $el; + this.shareToggles = document.documentElement.querySelectorAll('.sharing-checkbox-input'); + + this.$toggle.addEventListener('click', this.onToggleClick.bind(this)); + } + + onToggleClick () { + this.shareToggles.forEach(toggle => { + toggle.checked = !toggle.checked; + }) + } +} diff --git a/resources/assets/js/components/SimpleSelect.js b/resources/assets/js/components/SimpleSelect.js new file mode 100644 index 00000000..a5220383 --- /dev/null +++ b/resources/assets/js/components/SimpleSelect.js @@ -0,0 +1,8 @@ +export default class SimpleSelect { + + constructor ($el) { + $($el).selectize({ + create: false + }); + } +} diff --git a/resources/assets/js/components/TagsSelect.js b/resources/assets/js/components/TagsSelect.js new file mode 100644 index 00000000..5a3c9140 --- /dev/null +++ b/resources/assets/js/components/TagsSelect.js @@ -0,0 +1,59 @@ +export default class TagsSelect { + + constructor ($el) { + this.$el = $el; + + this.config = { + delimiter: ',', + persist: false, + load: (query, callback) => { + this.handleTagLoading(query, callback); + } + }; + + this.init(); + } + + init () { + this.prepareConfig(); + + $(this.$el).selectize(this.config); + } + + prepareConfig () { + if (this.selectAllowsCreation()) { + this.config.create = function (input) { + return { + value: input, + text: input + }; + }; + } + } + + selectAllowsCreation () { + return typeof this.$el.dataset.allowCreation !== 'undefined'; + } + + handleTagLoading (query, callback) { + if (!query.length) return callback(); + + const searchURL = window.appData.routes.ajax.searchTags; + + fetch(searchURL, { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + _token: window.appData.user.token, + query: query + }) + }).then((response) => { + return response.json(); + }).then((results) => { + callback(results); + }).catch(() => { + callback(); + }); + } +} diff --git a/resources/assets/sass/custom/_app.scss b/resources/assets/sass/custom/_app.scss index 05ae3ec3..cc03e8ea 100644 --- a/resources/assets/sass/custom/_app.scss +++ b/resources/assets/sass/custom/_app.scss @@ -70,5 +70,5 @@ body:not(.bookmarklet) { } #loader { - display: none; + display: none !important; } diff --git a/resources/assets/sass/loader.scss b/resources/assets/sass/loader.scss index 71d9a01b..b5fbd1e0 100644 --- a/resources/assets/sass/loader.scss +++ b/resources/assets/sass/loader.scss @@ -12,6 +12,10 @@ html, body { z-index: 9999; background-color: #fff; + @media (prefers-color-scheme: dark) { + background-color: #272B30; + } + div { justify-self: center; align-self: center; diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index ac5eea84..e900f957 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -56,7 +56,7 @@ 'cron_token_generate' => 'Generate Token', 'cron_token_generate_confirm' => 'Do you really want to generate a new token?', 'cron_token_help' => 'The cron token is needed to run the cron service which checks for dead links or running backups.', - 'cron_token_url' => 'Point your cron to the following URL: :route', + 'cron_token_url' => 'Point your cron to the following URL: :route', 'cron_token_generate_info' => 'Caution: If you already have an cron token, generating a new one will break the existing cron job!', 'cron_token_generate_failure' => 'A new cron token could not be generated. Please check your browser console and application logs for more information.', ]; diff --git a/resources/views/actions/bookmarklet/complete.blade.php b/resources/views/actions/bookmarklet/complete.blade.php index ea687ea3..11c435f0 100644 --- a/resources/views/actions/bookmarklet/complete.blade.php +++ b/resources/views/actions/bookmarklet/complete.blade.php @@ -12,15 +12,3 @@ @endsection - -@push('scripts') - -@endpush diff --git a/resources/views/actions/export/export.blade.php b/resources/views/actions/export/export.blade.php index b56d9549..be2f1ed8 100644 --- a/resources/views/actions/export/export.blade.php +++ b/resources/views/actions/export/export.blade.php @@ -24,15 +24,3 @@ @endsection - -@push('scripts') - -@endpush diff --git a/resources/views/actions/import/import.blade.php b/resources/views/actions/import/import.blade.php index 39d00fda..e6e2a4ba 100644 --- a/resources/views/actions/import/import.blade.php +++ b/resources/views/actions/import/import.blade.php @@ -37,15 +37,3 @@ class="form-control-file{{ $errors->has('import-file') ? ' is-invalid' : '' }}"> @endsection - -@push('scripts') - -@endpush diff --git a/resources/views/actions/search/partials/tags-js.blade.php b/resources/views/actions/search/partials/tags-js.blade.php deleted file mode 100644 index 6aab1aff..00000000 --- a/resources/views/actions/search/partials/tags-js.blade.php +++ /dev/null @@ -1,24 +0,0 @@ - diff --git a/resources/views/actions/search/search.blade.php b/resources/views/actions/search/search.blade.php index c61496fd..f1be07a5 100644 --- a/resources/views/actions/search/search.blade.php +++ b/resources/views/actions/search/search.blade.php @@ -80,8 +80,8 @@ class="custom-control-input" - +
@@ -126,7 +126,3 @@ class="custom-control-input"
@endsection - -@push('scripts') - @include('actions.search.partials.tags-js') -@endpush diff --git a/resources/views/actions/settings/partials/api.blade.php b/resources/views/actions/settings/partials/api.blade.php index 5e4f9ee7..e43847a0 100644 --- a/resources/views/actions/settings/partials/api.blade.php +++ b/resources/views/actions/settings/partials/api.blade.php @@ -2,22 +2,22 @@
@lang('settings.api_token')
-
+

@lang('settings.api_token_help')

-
-
-

@lang('settings.api_token_generate_failure')

@@ -25,37 +25,3 @@
- -@push('scripts') - -@endpush diff --git a/resources/views/actions/settings/partials/app-settings.blade.php b/resources/views/actions/settings/partials/app-settings.blade.php index 9d830f2a..a05f1bb2 100644 --- a/resources/views/actions/settings/partials/app-settings.blade.php +++ b/resources/views/actions/settings/partials/app-settings.blade.php @@ -15,7 +15,7 @@ @lang('settings.timezone') + class="simple-select {{ $errors->has('parent_category') ? ' is-invalid' : '' }}"> @foreach($categories as $category) @foreach($categories as $category) @if($category->childCategories->count() > 0) @@ -96,7 +96,7 @@ class="{{ $errors->has('category_id') ? ' is-invalid' : '' }}">
+ value="tags-select {{ old('tags', $link->tagsForInput()) }}" data-allow-creation="true"> @if ($errors->has('url'))
@endsection - -@push('scripts') - - @include('models.links.partials.tags-js') -@endpush diff --git a/resources/views/models/links/partials/create-form.blade.php b/resources/views/models/links/partials/create-form.blade.php index 28ea4f9c..b31b7f1c 100644 --- a/resources/views/models/links/partials/create-form.blade.php +++ b/resources/views/models/links/partials/create-form.blade.php @@ -59,7 +59,7 @@ class="form-control{{ $errors->has('title') ? ' is-invalid' : '' }}"