Compare commits

...

32 Commits

Author SHA1 Message Date
6fddb902e8 Merge 6566cc644e into 387224f830 2025-04-08 19:19:16 +02:00
387224f830 Merge pull request #8031 from wallabag/use-stimulus
Use Stimulus
2025-04-07 18:40:31 +02:00
e4fb100163 Remove jQuery 2025-04-07 14:56:06 +02:00
e28e1bddb4 Extract Entries Navigation controller 2025-04-07 14:56:06 +02:00
b266d6ca2f Fix esc shortcut after opening add url or search 2025-04-07 14:56:06 +02:00
8b0e6319e4 Extract Shortcuts controller 2025-04-07 14:56:06 +02:00
7eaaf5d38c Extract Dark Theme controller 2025-04-07 14:56:06 +02:00
a8cb9f4f77 Extract Leftbar and Add Tag controllers 2025-04-07 14:56:06 +02:00
c9cfae11f7 Extract Sticky Nav controller 2025-04-07 14:56:06 +02:00
2054be7bd4 Extract Topbar controller 2025-04-07 14:56:06 +02:00
f7c8466231 Extract Batch Edit controller 2025-04-07 14:56:06 +02:00
e438b5e63f Extract Tag controller 2025-04-07 14:56:06 +02:00
1fd861078d Extract Fake Radio controller 2025-04-07 14:56:06 +02:00
a06da68e72 Extract Config controller 2025-04-07 14:56:06 +02:00
9da9e6b004 Extract Clipboard controller 2025-04-07 14:56:06 +02:00
ffeca7f94d Extract Annotations controller 2025-04-07 14:56:06 +02:00
d515e11fe4 Extract Scroll Storage controller 2025-04-07 14:56:06 +02:00
019d252446 Extract Scroll Indicator controller 2025-04-07 14:56:06 +02:00
503b82ea13 Extract Highlight controller 2025-04-07 14:56:06 +02:00
3125eb43ad Extract QRCode controller 2025-04-07 14:56:06 +02:00
e5042074a2 Extract Materialize Toast controller 2025-04-07 14:56:06 +02:00
76c101938d Extract Materialize FAB controller 2025-04-07 14:56:06 +02:00
a7a4c5fefb Extract Materialize Form Select controller 2025-04-07 14:56:06 +02:00
1b683dbb05 Extract Materialize Dropdown controller 2025-04-07 14:56:06 +02:00
a69ea46945 Extract Materialize Collapsible controller 2025-04-07 14:56:06 +02:00
2a2172037e Extract Materialize Tooltip controller 2025-04-07 14:56:06 +02:00
0589066ed1 Extract Materialize Tabs controller 2025-04-07 14:56:06 +02:00
1f76184d02 Extract Materialize Sidenav controller 2025-04-07 14:56:06 +02:00
66c6a25941 Install Stimulus 2025-04-07 14:56:06 +02:00
46f505f69f Merge pull request #8124 from wallabag/phpstan-level-4
PHPStan level 4
2025-04-07 14:20:14 +02:00
b4483023e6 Move to PHPStan level 4 2025-04-07 12:20:06 +02:00
31e1be4191 Add phpstan-baseline target in Makefile 2025-04-07 11:42:05 +02:00
79 changed files with 900 additions and 795 deletions

View File

@ -52,6 +52,9 @@ fix-cs: ## Run PHP-CS-Fixer
phpstan: ## Run PHPStan
@$(PHP_NO_XDEBUG) bin/phpstan analyse
phpstan-baseline: ## Generate PHPStan baseline
@$(PHP_NO_XDEBUG) bin/phpstan analyse --generate-baseline
lint-js: ## Run ESLint
@$(YARN) lint:js

11
assets/bootstrap.js vendored Normal file
View File

@ -0,0 +1,11 @@
import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export default startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.[jt]sx?$/,
));
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

4
assets/controllers.json Normal file
View File

@ -0,0 +1,4 @@
{
"controllers": [],
"entrypoints": []
}

View File

@ -0,0 +1,13 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['input'];
toggle() {
this.element.classList.toggle('hidden');
if (!this.element.classList.contains('hidden')) {
this.inputTarget.focus();
}
}
}

View File

@ -0,0 +1,57 @@
import { Controller } from '@hotwired/stimulus';
import annotator from 'annotator';
export default class extends Controller {
static values = {
entryId: Number,
createUrl: String,
updateUrl: String,
destroyUrl: String,
searchUrl: String,
};
connect() {
this.app = new annotator.App();
this.app.include(annotator.ui.main, {
element: this.element,
});
const authorization = {
permits() { return true; },
};
this.app.registry.registerUtility(authorization, 'authorizationPolicy');
this.app.include(annotator.storage.http, {
prefix: '',
urls: {
create: this.createUrlValue,
update: this.updateUrlValue,
destroy: this.destroyUrlValue,
search: this.searchUrlValue,
},
entryId: this.entryIdValue,
onError(msg, xhr) {
if (!Object.prototype.hasOwnProperty.call(xhr, 'responseJSON')) {
annotator.notification.banner('An error occurred', 'error');
return;
}
Object.values(xhr.responseJSON.children).forEach((v) => {
if (v.errors) {
Object.values(v.errors).forEach((errorText) => {
annotator.notification.banner(errorText, 'error');
});
}
});
},
});
this.app.start().then(() => {
this.app.annotations.load({ entry: this.entryIdValue });
});
}
disconnect() {
this.app.destroy();
}
}

View File

@ -0,0 +1,15 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['item', 'tagAction'];
toggleSelection(e) {
this.itemTargets.forEach((item) => {
item.checked = e.currentTarget.checked; // eslint-disable-line no-param-reassign
});
}
tagSelection() {
this.element.requestSubmit(this.tagActionTarget);
}
}

View File

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
import ClipboardJS from 'clipboard';
export default class extends Controller {
connect() {
this.clipboard = new ClipboardJS(this.element);
this.clipboard.on('success', (e) => {
e.clearSelection();
});
}
disconnect() {
this.clipboard.destroy();
}
}

View File

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['previewArticle', 'previewContent', 'font', 'fontSize', 'lineHeight', 'maxWidth'];
connect() {
this.updatePreview();
}
updatePreview() {
this.previewArticleTarget.style.maxWidth = `${this.maxWidthTarget.value}em`;
this.previewContentTarget.style.fontFamily = this.fontTarget.value;
this.previewContentTarget.style.fontSize = `${this.fontSizeTarget.value}em`;
this.previewContentTarget.style.lineHeight = `${this.lineHeightTarget.value}em`;
}
}

View File

@ -0,0 +1,39 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.#choose();
this.mql = window.matchMedia('(prefers-color-scheme: dark)');
this.mql.addEventListener('change', this.#choose.bind(this));
}
useLight() {
this.element.classList.remove('dark-theme');
document.cookie = 'theme=light;samesite=Lax;path=/;max-age=31536000';
}
useDark() {
this.element.classList.add('dark-theme');
document.cookie = 'theme=dark;samesite=Lax;path=/;max-age=31536000';
}
useAuto() {
document.cookie = 'theme=auto;samesite=Lax;path=/;max-age=0';
this.#choose();
}
#choose() {
const themeCookieExists = document.cookie.split(';').some((cookie) => cookie.trim().startsWith('theme='));
if (themeCookieExists) {
return;
}
if (this.mql.matches) {
this.element.classList.add('dark-theme');
} else {
this.element.classList.remove('dark-theme');
}
}
}

View File

@ -0,0 +1,58 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['card', 'paginationWrapper'];
connect() {
this.pagination = this.paginationWrapperTarget.querySelector('.pagination');
this.cardIndex = 0;
this.lastCardIndex = this.cardTargets.length - 1;
/* If we come from next page */
if (window.location.hash === '#prev') {
this.cardIndex = this.lastCardIndex;
}
this.currentCard = this.cardTargets[this.cardIndex];
this.currentCard.classList.add('z-depth-4');
}
selectRightCard() {
if (this.cardIndex >= 0 && this.cardIndex < this.lastCardIndex) {
this.currentCard.classList.remove('z-depth-4');
this.cardIndex += 1;
this.currentCard = this.cardTargets[this.cardIndex];
this.currentCard.classList.add('z-depth-4');
return;
}
if (this.pagination && this.pagination.querySelector('a[rel="next"]')) {
window.location.href = this.pagination.querySelector('a[rel="next"]').href;
}
}
selectLeftCard() {
if (this.cardIndex > 0 && this.cardIndex <= this.lastCardIndex) {
this.currentCard.classList.remove('z-depth-4');
this.cardIndex -= 1;
this.currentCard = this.cardTargets[this.cardIndex];
this.currentCard.classList.add('z-depth-4');
return;
}
if (this.pagination && this.pagination.querySelector('a[rel="prev"]')) {
window.location.href = `${this.pagination.querySelector('a[rel="prev"]').href}#prev`;
}
}
selectCurrentCard() {
const url = this.currentCard.querySelector('a.card-title').href;
if (url) {
window.location.href = url;
}
}
}

View File

@ -0,0 +1,13 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['emailTwoFactor', 'googleTwoFactor'];
uncheckGoogle() {
this.googleTwoFactorTarget.checked = false;
}
uncheckEmail() {
this.emailTwoFactorTarget.checked = false;
}
}

View File

@ -0,0 +1,11 @@
import { Controller } from '@hotwired/stimulus';
import 'highlight.js/styles/atom-one-light.css';
import hljs from 'highlight.js';
export default class extends Controller {
connect() {
this.element.querySelectorAll('pre code').forEach((element) => {
hljs.highlightElement(element);
});
}
}

View File

@ -0,0 +1,7 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
toggleAddTagForm() {
this.dispatch('toggleAddTagForm');
}
}

View File

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
static values = {
accordion: { type: Boolean, default: true },
};
connect() {
this.instance = M.Collapsible.init(this.element, { accordion: this.accordionValue });
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
connect() {
this.instance = M.Dropdown.init(this.element, {
hover: false,
coverTrigger: false,
constrainWidth: false,
});
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,32 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
static values = {
edge: { type: String, default: 'left' },
};
connect() {
this.instance = M.FloatingActionButton.init(this.element);
}
autoDisplay() {
const scrolled = (window.innerHeight + window.scrollY) >= document.body.offsetHeight;
if (scrolled) {
this.toggleScroll = true;
this.instance.open();
} else if (this.toggleScroll === true) {
this.toggleScroll = false;
this.instance.close();
}
}
click() {
this.dispatch('click');
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
connect() {
this.instance = M.FormSelect.init(this.element.querySelector('select'));
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,24 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
const mobileMaxWidth = 993;
export default class extends Controller {
static values = {
edge: { type: String, default: 'left' },
};
connect() {
this.instance = M.Sidenav.init(this.element, { edge: this.edgeValue });
}
close() {
if (window.innerWidth < mobileMaxWidth) {
this.instance.close();
}
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
connect() {
this.instance = M.Tabs.init(this.element);
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
connect() {
this.instance = M.toast({ text: this.element.innerText });
}
disconnect() {
this.instance.dismissAll();
}
}

View File

@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
import M from '@materializecss/materialize';
export default class extends Controller {
connect() {
this.instance = M.Tooltip.init(this.element);
}
disconnect() {
this.instance.destroy();
}
}

View File

@ -0,0 +1,10 @@
import { Controller } from '@hotwired/stimulus';
import jrQrcode from 'jr-qrcode';
export default class extends Controller {
static values = { url: String };
connect() {
this.element.setAttribute('src', jrQrcode.getQrBase64(this.urlValue));
}
}

View File

@ -0,0 +1,10 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
updateWidth() {
const referenceHeight = document.body.offsetHeight - window.innerHeight;
const scrollPercent = (window.scrollY / referenceHeight) * 100;
this.element.style.width = `${scrollPercent}%`;
}
}

View File

@ -0,0 +1,19 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static values = { entryId: Number };
connect() {
window.scrollTo({
top: window.innerHeight * localStorage[`wallabag.article.${this.entryIdValue}.percent`],
behavior: 'smooth',
});
}
saveScroll() {
const scrollPercent = window.scrollY / window.innerHeight;
const scrollPercentRounded = Math.round(scrollPercent * 100) / 100;
localStorage[`wallabag.article.${this.entryIdValue}.percent`] = scrollPercentRounded;
}
}

View File

@ -0,0 +1,141 @@
import { Controller } from '@hotwired/stimulus';
import Mousetrap from 'mousetrap';
export default class extends Controller {
static targets = ['openOriginal', 'markAsFavorite', 'markAsRead', 'deleteEntry', 'showAddUrl', 'showSearch', 'showActions'];
static outlets = ['entries-navigation'];
connect() {
/* Go to */
Mousetrap.bind('g u', () => {
window.location.href = Routing.generate('homepage');
});
Mousetrap.bind('g s', () => {
window.location.href = Routing.generate('starred');
});
Mousetrap.bind('g r', () => {
window.location.href = Routing.generate('archive');
});
Mousetrap.bind('g a', () => {
window.location.href = Routing.generate('all');
});
Mousetrap.bind('g t', () => {
window.location.href = Routing.generate('tag');
});
Mousetrap.bind('g c', () => {
window.location.href = Routing.generate('config');
});
Mousetrap.bind('g i', () => {
window.location.href = Routing.generate('import');
});
Mousetrap.bind('g d', () => {
window.location.href = Routing.generate('developer');
});
Mousetrap.bind('?', () => {
window.location.href = Routing.generate('howto');
});
Mousetrap.bind('g l', () => {
window.location.href = Routing.generate('fos_user_security_logout');
});
/* open original article */
Mousetrap.bind('o', () => {
if (!this.hasOpenOriginalTarget) {
return;
}
this.openOriginalTarget.click();
});
/* mark as favorite */
Mousetrap.bind('f', () => {
if (!this.hasMarkAsFavoriteTarget) {
return;
}
this.markAsFavoriteTarget.click();
});
/* mark as read */
Mousetrap.bind('a', () => {
if (!this.hasMarkAsReadTarget) {
return;
}
this.markAsReadTarget.click();
});
/* delete */
Mousetrap.bind('del', () => {
if (!this.hasDeleteEntryTarget) {
return;
}
this.deleteEntryTarget.click();
});
/* Actions */
Mousetrap.bind('g n', (e) => {
if (!this.hasShowAddUrlTarget) {
return;
}
e.preventDefault();
this.showAddUrlTarget.click();
});
Mousetrap.bind('s', (e) => {
if (!this.hasShowSearchTarget) {
return;
}
e.preventDefault();
this.showSearchTarget.click();
});
Mousetrap.bind('esc', (e) => {
if (!this.hasShowActionsTarget) {
return;
}
e.preventDefault();
this.showActionsTarget.click();
});
const originalStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = (e, element, combo) => {
// allow esc key to be used in input fields of topbar
if (combo === 'esc' && element.dataset.topbarTarget !== undefined) {
return false;
}
return originalStopCallback(e, element);
};
Mousetrap.bind('right', () => {
if (!this.hasEntriesNavigationOutlet) {
return;
}
this.entriesNavigationOutlet.selectRightCard();
});
Mousetrap.bind('left', () => {
if (!this.hasEntriesNavigationOutlet) {
return;
}
this.entriesNavigationOutlet.selectLeftCard();
});
Mousetrap.bind('enter', () => {
if (!this.hasEntriesNavigationOutlet) {
return;
}
this.entriesNavigationOutlet.selectCurrentCard();
});
}
}

View File

@ -0,0 +1,7 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
toggle() {
this.element.classList.toggle('entry-nav-top--sticky');
}
}

View File

@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['link', 'edit', 'form', 'input'];
showForm() {
this.formTarget.classList.remove('hidden');
this.editTarget.classList.add('hidden');
this.linkTarget.classList.add('hidden');
this.inputTarget.focus();
}
}

View File

@ -0,0 +1,31 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['addUrl', 'addUrlInput', 'search', 'searchInput', 'actions'];
showAddUrl() {
this.actionsTarget.style.display = 'none';
this.addUrlTarget.style.display = 'flex';
this.searchTarget.style.display = 'none';
this.addUrlInputTarget.focus();
}
submittingUrl(e) {
e.currentTarget.disabled = true;
this.addUrlInputTarget.readOnly = true;
this.addUrlInputTarget.blur();
}
showSearch() {
this.actionsTarget.style.display = 'none';
this.addUrlTarget.style.display = 'none';
this.searchTarget.style.display = 'flex';
this.searchInputTarget.focus();
}
showActions() {
this.actionsTarget.style.display = 'flex';
this.addUrlTarget.style.display = 'none';
this.searchTarget.style.display = 'none';
}
}

View File

@ -1,18 +1,11 @@
import $ from 'jquery';
import './bootstrap';
/* Materialize imports */
import '@materializecss/materialize/dist/css/materialize.css';
import M from '@materializecss/materialize/dist/js/materialize';
import '@materializecss/materialize/dist/js/materialize';
/* Annotations */
import annotator from 'annotator';
import ClipboardJS from 'clipboard';
import 'mathjax/es5/tex-svg';
/* jrQrcode */
import jrQrcode from 'jr-qrcode';
/* Fonts */
import 'material-design-icons-iconfont/dist/material-design-icons.css';
import 'lato-font/css/lato-font.css';
@ -22,371 +15,5 @@ import '@fontsource/eb-garamond';
import '@fontsource/montserrat';
import '@fontsource/oswald';
/* Highlight */
import './js/highlight';
/* Tools */
import {
savePercent, retrievePercent, initPreviewText,
} from './js/tools';
/* Import shortcuts */
import './js/shortcuts/main';
import './js/shortcuts/entry';
/* Theme style */
import './scss/index.scss';
const mobileMaxWidth = 993;
/* ==========================================================================
Annotations & Remember position
========================================================================== */
$(document).ready(() => {
if ($('#article').length) {
const app = new annotator.App();
app.include(annotator.ui.main, {
element: document.querySelector('article'),
});
const authorization = {
permits() { return true; },
};
app.registry.registerUtility(authorization, 'authorizationPolicy');
const x = JSON.parse($('#annotationroutes').html());
app.include(annotator.storage.http, $.extend({}, x, {
onError(msg, xhr) {
if (!Object.prototype.hasOwnProperty.call(xhr, 'responseJSON')) {
annotator.notification.banner('An error occurred', 'error');
return;
}
$.each(xhr.responseJSON.children, (k, v) => {
if (v.errors) {
$.each(v.errors, (n, errorText) => {
annotator.notification.banner(errorText, 'error');
});
}
});
},
}));
app.start().then(() => {
app.annotations.load({ entry: x.entryId });
});
$(window).scroll(() => {
const scrollTop = $(window).scrollTop();
const docHeight = $(document).height();
const scrollPercent = (scrollTop) / (docHeight);
const scrollPercentRounded = Math.round(scrollPercent * 100) / 100;
savePercent(x.entryId, scrollPercentRounded);
});
retrievePercent(x.entryId);
$(window).resize(() => {
retrievePercent(x.entryId, true);
});
}
document.querySelectorAll('[data-handler=tag-rename]').forEach((item) => {
const current = item;
current.wallabag_edit_mode = false;
current.onclick = (event) => {
const target = event.currentTarget;
if (target.wallabag_edit_mode === false) {
$(target.parentNode.querySelector('[data-handle=tag-link]')).addClass('hidden');
$(target.parentNode.querySelector('[data-handle=tag-rename-form]')).removeClass('hidden');
target.parentNode.querySelector('[data-handle=tag-rename-form] input').focus();
target.querySelector('.material-icons').innerHTML = 'done';
target.wallabag_edit_mode = true;
} else {
target.parentNode.querySelector('[data-handle=tag-rename-form]').submit();
}
};
});
// mimic radio button because emailTwoFactor is a boolean
$('#update_user_googleTwoFactor').on('change', () => {
$('#update_user_emailTwoFactor').prop('checked', false);
});
$('#update_user_emailTwoFactor').on('change', () => {
$('#update_user_googleTwoFactor').prop('checked', false);
});
// same mimic for super admin
$('#user_googleTwoFactor').on('change', () => {
$('#user_emailTwoFactor').prop('checked', false);
});
$('#user_emailTwoFactor').on('change', () => {
$('#user_googleTwoFactor').prop('checked', false);
});
// handle copy to clipboard for developer stuff
const clipboard = new ClipboardJS('.btn');
clipboard.on('success', (e) => {
e.clearSelection();
});
});
(function darkTheme() {
const rootEl = document.querySelector('html');
const themeDom = {
darkClass: 'dark-theme',
toggleClass(el) {
return el.classList.toggle(this.darkClass);
},
addClass(el) {
return el.classList.add(this.darkClass);
},
removeClass(el) {
return el.classList.remove(this.darkClass);
},
};
const themeCookie = {
values: {
light: 'light',
dark: 'dark',
},
name: 'theme',
getValue(isDarkTheme) {
return isDarkTheme ? this.values.dark : this.values.light;
},
setCookie(isDarkTheme) {
const value = this.getValue(isDarkTheme);
document.cookie = `${this.name}=${value};samesite=Lax;path=/;max-age=31536000`;
},
removeCookie() {
document.cookie = `${this.name}=auto;samesite=Lax;path=/;max-age=0`;
},
exists() {
return document.cookie.split(';').some((cookie) => cookie.trim().startsWith(`${this.name}=`));
},
};
const preferedColorScheme = {
choose() {
const themeCookieExists = themeCookie.exists();
if (this.isAvailable() && !themeCookieExists) {
const isPreferedColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)').matches === true;
if (!themeCookieExists) {
themeDom[isPreferedColorSchemeDark ? 'addClass' : 'removeClass'](rootEl);
}
}
},
isAvailable() {
return typeof window.matchMedia === 'function';
},
init() {
if (!this.isAvailable()) {
return false;
}
this.choose();
window.matchMedia('(prefers-color-scheme: dark)').addListener(() => {
this.choose();
});
return true;
},
};
const addDarkThemeListeners = () => {
$(document).ready(() => {
const lightThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="light"]');
[...lightThemeButtons].map((lightThemeButton) => {
lightThemeButton.addEventListener('click', (e) => {
e.preventDefault();
themeDom.removeClass(rootEl);
themeCookie.setCookie(false);
});
return true;
});
const darkThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="dark"]');
[...darkThemeButtons].map((darkThemeButton) => {
darkThemeButton.addEventListener('click', (e) => {
e.preventDefault();
themeDom.addClass(rootEl);
themeCookie.setCookie(true);
});
return true;
});
const autoThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="auto"]');
[...autoThemeButtons].map((autoThemeButton) => {
autoThemeButton.addEventListener('click', (e) => {
e.preventDefault();
themeCookie.removeCookie();
preferedColorScheme.choose();
});
return true;
});
});
};
preferedColorScheme.init();
addDarkThemeListeners();
}());
const stickyNav = () => {
const nav = $('.js-entry-nav-top');
$('[data-toggle="actions"]').click(() => {
nav.toggleClass('entry-nav-top--sticky');
});
};
const articleScroll = () => {
const articleEl = $('#article');
if (articleEl.length > 0) {
$(window).scroll(() => {
const s = $(window).scrollTop();
const d = $(document).height();
const c = $(window).height();
const articleElBottom = articleEl.offset().top + articleEl.height();
const scrollPercent = (s / (d - c)) * 100;
$('.progress .determinate').css('width', `${scrollPercent}%`);
const fixedActionBtn = $('.js-fixed-action-btn');
const toggleScrollDataName = 'toggle-auto';
if ((s + c) > articleElBottom) {
fixedActionBtn.data(toggleScrollDataName, true);
fixedActionBtn.floatingActionButton('open');
} else if (fixedActionBtn.data(toggleScrollDataName) === true) {
fixedActionBtn.data(toggleScrollDataName, false);
fixedActionBtn.floatingActionButton('close');
}
});
}
};
$(document).ready(() => {
// sidenav
document.querySelectorAll('.sidenav').forEach((element) => {
$(element).sidenav({ edge: element.getAttribute('data-edge') ?? 'left' });
});
$('select').formSelect();
$('.collapsible[data-collapsible="accordion"]').collapsible();
$('.collapsible[data-collapsible="expandable"]').collapsible({
accordion: false,
});
$('.dropdown-trigger').dropdown({ hover: false });
$('.dropdown-trigger[data-covertrigger="false"][data-constrainwidth="false"]').dropdown({
hover: false,
coverTrigger: false,
constrainWidth: false,
});
$('.tabs').tabs();
$('.tooltipped').tooltip();
$('.fixed-action-btn').floatingActionButton();
stickyNav();
articleScroll();
initPreviewText();
const toggleNav = (toShow, toFocus) => {
$('.nav-panel-actions').hide();
$(toShow).show();
$(toFocus).focus();
};
$('#nav-btn-add-tag').on('click', () => {
$('.nav-panel-add-tag').toggle();
$('.nav-panel-menu').addClass('hidden');
if (window.innerWidth < mobileMaxWidth) {
$('.sidenav').sidenav('close');
}
$('#tag_label').focus();
return false;
});
$('#nav-btn-add').on('click', () => {
toggleNav('.nav-panel-add', '#entry_url');
return false;
});
$('#config_fontsize').on('input', () => {
const value = $('#config_fontsize').val();
const css = `${value}em`;
$('#preview-content').css('font-size', css);
});
$('#config_font').on('change', () => {
const value = $('#config_font').val();
$('#preview-content').css('font-family', value);
});
$('#config_lineHeight').on('input', () => {
const value = $('#config_lineHeight').val();
const css = `${value}em`;
$('#preview-content').css('line-height', css);
});
$('#config_maxWidth').on('input', () => {
const value = $('#config_maxWidth').val();
const css = `${value}em`;
$('#preview-article').css('max-width', css);
});
const materialAddForm = $('.nav-panel-add');
materialAddForm.on('submit', () => {
materialAddForm.addClass('disabled');
$('input#entry_url', materialAddForm).prop('readonly', true).trigger('blur');
});
$('#nav-btn-search').on('click', () => {
toggleNav('.nav-panel-search', '#search_entry_term');
return false;
});
$('.close').on('click', (e) => {
$(e.target).parent('.nav-panel-item').hide();
$('.nav-panel-actions').show();
return false;
});
const mainCheckboxes = document.querySelectorAll('[data-js="checkboxes-toggle"]');
if (mainCheckboxes.length) {
[...mainCheckboxes].forEach((el) => {
el.addEventListener('click', () => {
const checkboxes = document.querySelectorAll(el.dataset.toggle);
[...checkboxes].forEach((checkbox) => {
const checkboxClone = checkbox;
checkboxClone.checked = el.checked;
});
});
});
}
$('form[name="form_mass_action"] input[name="tags"]').on('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
$('form[name="form_mass_action"] button[name="tag"]').trigger('click');
}
});
document.querySelectorAll('img.jr-qrcode').forEach((qrcode) => {
const src = jrQrcode.getQrBase64(qrcode.getAttribute('data-url'));
qrcode.setAttribute('src', src);
});
document.querySelectorAll('.material-toast').forEach((toast) => {
M.toast({
text: toast.innerText,
});
});
});

View File

@ -1,8 +0,0 @@
import 'highlight.js/styles/atom-one-light.css';
import hljs from 'highlight.js';
window.addEventListener('load', () => {
document.querySelectorAll('pre').forEach((element) => {
hljs.highlightElement(element);
});
});

View File

@ -1,26 +0,0 @@
import Mousetrap from 'mousetrap';
import $ from 'jquery';
$(document).ready(() => {
if ($('#article').length > 0) {
/* open original article */
Mousetrap.bind('o', () => {
$('ul.sidenav a.original i')[0].click();
});
/* mark as favorite */
Mousetrap.bind('f', () => {
$('ul.sidenav a.favorite i')[0].click();
});
/* mark as read */
Mousetrap.bind('a', () => {
$('ul.sidenav a.markasread i')[0].click();
});
/* delete */
Mousetrap.bind('del', () => {
$('ul.sidenav a.delete i')[0].click();
});
}
});

View File

@ -1,104 +0,0 @@
import Mousetrap from 'mousetrap';
import $ from 'jquery';
/* Go to */
Mousetrap.bind('g u', () => { window.location.href = Routing.generate('homepage'); });
Mousetrap.bind('g s', () => { window.location.href = Routing.generate('starred'); });
Mousetrap.bind('g r', () => { window.location.href = Routing.generate('archive'); });
Mousetrap.bind('g a', () => { window.location.href = Routing.generate('all'); });
Mousetrap.bind('g t', () => { window.location.href = Routing.generate('tag'); });
Mousetrap.bind('g c', () => { window.location.href = Routing.generate('config'); });
Mousetrap.bind('g i', () => { window.location.href = Routing.generate('import'); });
Mousetrap.bind('g d', () => { window.location.href = Routing.generate('developer'); });
Mousetrap.bind('?', () => { window.location.href = Routing.generate('howto'); });
Mousetrap.bind('g l', () => { window.location.href = Routing.generate('fos_user_security_logout'); });
function toggleFocus(cardToToogleFocus) {
if (cardToToogleFocus) {
$(cardToToogleFocus).toggleClass('z-depth-4');
}
}
$(document).ready(() => {
const cards = $('#content').find('.card');
const cardNumber = cards.length;
let cardIndex = 0;
/* If we come from next page */
if (window.location.hash === '#prev') {
cardIndex = cardNumber - 1;
}
let card = cards[cardIndex];
const pagination = $('.pagination');
/* Show nothing on quickstart */
if ($('#content > div.quickstart').length > 0) {
return;
}
/* Show nothing on login/register page */
if ($('#username').length > 0 || $('#fos_user_registration_form_username').length > 0) {
return;
}
/* Show nothing on login/register page */
if ($('#username').length > 0 || $('#fos_user_registration_form_username').length > 0) {
return;
}
/* Focus current card */
toggleFocus(card);
/* Actions */
Mousetrap.bind('g n', () => {
$('#nav-btn-add').trigger('click');
return false;
});
Mousetrap.bind('s', () => {
$('#nav-btn-search').trigger('click');
return false;
});
Mousetrap.bind('esc', () => {
$('.close').trigger('click');
});
/* Select right card. If there's a next page, go to next page */
Mousetrap.bind('right', () => {
if (cardIndex >= 0 && cardIndex < cardNumber - 1) {
toggleFocus(card);
cardIndex += 1;
card = cards[cardIndex];
toggleFocus(card);
return;
}
if (pagination.length > 0 && pagination.find('li.next:not(.disabled)').length > 0 && cardIndex === cardNumber - 1) {
window.location.href = window.location.origin + $(pagination).find('li.next a').attr('href');
}
});
/* Select previous card. If there's a previous page, go to next page */
Mousetrap.bind('left', () => {
if (cardIndex > 0 && cardIndex < cardNumber) {
toggleFocus(card);
cardIndex -= 1;
card = cards[cardIndex];
toggleFocus(card);
return;
}
if (pagination.length > 0 && $(pagination).find('li.prev:not(.disabled)').length > 0 && cardIndex === 0) {
window.location.href = `${window.location.origin + $(pagination).find('li.prev a').attr('href')}#prev`;
}
});
Mousetrap.bind('enter', () => {
if (typeof card !== 'object') {
return;
}
const url = $(card).find('.card-title a').attr('href');
if (typeof url === 'string' && url.length > 0) {
window.location.href = window.location.origin + url;
}
});
});

View File

@ -1,54 +0,0 @@
import $ from 'jquery';
function supportsLocalStorage() {
try {
return 'localStorage' in window && window.localStorage !== null;
} catch (e) {
return false;
}
}
function savePercent(id, percent) {
if (!supportsLocalStorage()) { return false; }
localStorage[`wallabag.article.${id}.percent`] = percent;
return true;
}
function retrievePercent(id, resized) {
if (!supportsLocalStorage()) { return false; }
const bheight = $(document).height();
const percent = localStorage[`wallabag.article.${id}.percent`];
const scroll = bheight * percent;
if (!resized) {
window.scrollTo({
top: scroll,
behavior: 'smooth',
});
}
return true;
}
function initPreviewText() {
// no display if preview_text not available
if ($('div').is('#preview-article')) {
const defaultFontFamily = $('#config_font').val();
const defaultFontSize = $('#config_fontsize').val();
const defaultLineHeight = $('#config_lineHeight').val();
const defaultMaxWidth = $('#config_maxWidth').val();
const previewContent = $('#preview-content');
previewContent.css('font-family', defaultFontFamily);
previewContent.css('font-size', `${defaultFontSize}em`);
previewContent.css('line-height', `${defaultLineHeight}em`);
$('#preview-article').css('max-width', `${defaultMaxWidth}em`);
}
}
export {
savePercent,
retrievePercent,
initPreviewText,
};

View File

@ -186,6 +186,14 @@ a.original:not(.waves-effect) {
color: #fff;
}
.card-tag-labels button {
background: transparent;
border: none;
font-weight: normal;
color: #fff;
cursor: pointer;
}
.card-tag-link {
width: calc(100% - 24px);
line-height: 1.3;
@ -196,6 +204,7 @@ a.original:not(.waves-effect) {
.card-tag-form {
display: flex;
align-items: center;
min-width: 100px;
flex-grow: 1;
}

View File

@ -80,14 +80,14 @@
"@fontsource/eb-garamond": "^5.2.5",
"@fontsource/montserrat": "^5.2.5",
"@fontsource/oswald": "^5.2.5",
"@hotwired/stimulus": "^3.2.2",
"@materializecss/materialize": "^1.2.2",
"@symfony/stimulus-bridge": "^4.0.0",
"annotator": "wallabag/annotator#master",
"clipboard": "^2.0.11",
"hammerjs": "^2.0.8",
"highlight.js": "^11.11.1",
"icomoon-free-npm": "^0.0.0",
"jquery": "^3.7.1",
"jquery.cookie": "^1.4.1",
"jr-qrcode": "^1.2.1",
"material-design-icons-iconfont": "^6.7.0",
"mathjax": "^3.2.2",
@ -100,7 +100,7 @@
"build:dev": "encore dev",
"watch": "encore dev --watch",
"build:prod": "encore production --progress",
"lint:js": "eslint assets/*.js assets/js/*.js assets/js/**/*.js",
"lint:js": "eslint assets/*.js assets/controllers/*.js",
"lint:scss": "stylelint assets/scss/*.scss assets/scss/**/*.scss"
}
}

View File

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#"
count: 1
path: src/ParamConverter/UsernameFeedTokenConverter.php

View File

@ -2,7 +2,7 @@ includes:
- phpstan-baseline.neon
parameters:
level: 3
level: 4
paths:
- src
- tests

View File

@ -554,7 +554,7 @@ class EntryRestController extends WallabagRestController
$this->entityManager->persist($entry);
$this->entityManager->flush();
$results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false;
$results[$key]['entry'] = $entry->getId();
// entry saved, dispatch event about it!
$eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
@ -734,7 +734,7 @@ class EntryRestController extends WallabagRestController
'html' => !empty($data['content']) ? $data['content'] : $entry->getContent(),
'url' => $entry->getUrl(),
'language' => !empty($data['language']) ? $data['language'] : $entry->getLanguage(),
'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt(),
'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt()?->format('Y-m-d H:i:s') ?? '',
// faking the open graph preview picture
'image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(),
'authors' => \is_string($data['authors']) ? explode(',', $data['authors']) : $entry->getPublishedBy(),

View File

@ -125,10 +125,6 @@ class FeedController extends AbstractController
$perPage = $user->getConfig()->getFeedLimit() ?: $this->getParameter('wallabag.feed_limit');
$entries->setMaxPerPage($perPage);
if (null === $entries) {
throw $this->createNotFoundException('No entries found?');
}
try {
$entries->setCurrentPage($page);
} catch (OutOfRangeCurrentPageException) {

View File

@ -61,10 +61,10 @@ class PocketController extends AbstractController
return $this->redirect($this->generateUrl('import_pocket'));
}
$form = $request->request->get('form');
$form = $request->request->all('form');
$this->session->set('import.pocket.code', $requestToken);
if (null !== $form && \array_key_exists('mark_as_read', $form)) {
if (\array_key_exists('mark_as_read', $form)) {
$this->session->set('mark_as_read', $form['mark_as_read']);
}

View File

@ -3,6 +3,7 @@
namespace Wallabag\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation\Exclude;
@ -233,11 +234,14 @@ class Entry
#[Groups(['export_all'])]
private $user;
/**
* @var Collection<Tag>
*/
#[ORM\JoinTable(name: 'entry_tag')]
#[ORM\JoinColumn(name: 'entry_id', referencedColumnName: 'id', onDelete: 'cascade')]
#[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'cascade')]
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'entries', cascade: ['persist'])]
private $tags;
private Collection $tags;
/*
* @param User $user
@ -589,7 +593,7 @@ class Entry
}
/**
* @return string
* @return string|null
*/
public function getDomainName()
{
@ -597,7 +601,7 @@ class Entry
}
/**
* @param string $domainName
* @param string|null $domainName
*/
public function setDomainName($domainName)
{
@ -605,7 +609,7 @@ class Entry
}
/**
* @return ArrayCollection
* @return Collection
*/
public function getTags()
{
@ -696,7 +700,7 @@ class Entry
/**
* Get previewPicture.
*
* @return string
* @return string|null
*/
public function getPreviewPicture()
{
@ -706,7 +710,7 @@ class Entry
/**
* Set language.
*
* @param string $language
* @param string|null $language
*
* @return Entry
*/
@ -720,7 +724,7 @@ class Entry
/**
* Get language.
*
* @return string
* @return string|null
*/
public function getLanguage()
{
@ -808,7 +812,7 @@ class Entry
}
/**
* @return \DateTimeInterface
* @return \DateTimeInterface|null
*/
public function getPublishedAt()
{

View File

@ -3,6 +3,7 @@
namespace Wallabag\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use JMS\Serializer\Annotation\ExclusionPolicy;
@ -41,8 +42,11 @@ class Tag implements \Stringable
#[Expose]
private $slug;
/**
* @var Collection<Entry>
*/
#[ORM\ManyToMany(targetEntity: Entry::class, mappedBy: 'tags', cascade: ['persist'])]
private $entries;
private Collection $entries;
public function __construct()
{
@ -121,7 +125,7 @@ class Tag implements \Stringable
/**
* Get entries for this tag.
*
* @return ArrayCollection<Entry>
* @return Collection<Entry>
*/
public function getEntries()
{

View File

@ -3,6 +3,7 @@
namespace Wallabag\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation\Accessor;
@ -117,16 +118,16 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI
protected $config;
/**
* @var ArrayCollection&iterable<SiteCredential>
* @var Collection<SiteCredential>
*/
#[ORM\OneToMany(targetEntity: SiteCredential::class, mappedBy: 'user', cascade: ['remove'])]
protected $siteCredentials;
protected Collection $siteCredentials;
/**
* @var ArrayCollection&iterable<Client>
* @var Collection<Client>
*/
#[ORM\OneToMany(targetEntity: Client::class, mappedBy: 'user', cascade: ['remove'])]
protected $clients;
protected Collection $clients;
/**
* @see getFirstClient() below
@ -162,6 +163,8 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI
{
parent::__construct();
$this->entries = new ArrayCollection();
$this->siteCredentials = new ArrayCollection();
$this->clients = new ArrayCollection();
$this->roles = ['ROLE_USER'];
}
@ -216,7 +219,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI
}
/**
* @return ArrayCollection<Entry>
* @return Collection<Entry>
*/
public function getEntries()
{
@ -338,13 +341,15 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI
*/
public function addClient(Client $client)
{
$this->clients[] = $client;
if (!$this->clients->contains($client)) {
$this->clients->add($client);
}
return $this;
}
/**
* @return ArrayCollection<Client>
* @return Collection<Client>
*/
public function getClients()
{
@ -358,7 +363,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI
*/
public function getFirstClient()
{
if (!empty($this->clients)) {
if (!$this->clients->isEmpty()) {
return $this->clients->first();
}

View File

@ -79,7 +79,7 @@ class DownloadImagesSubscriber implements EventSubscriberInterface
*
* @todo If we want to add async download, it should be done in that method
*
* @return string|false False in case of async
* @return string
*/
private function downloadImages(Entry $entry)
{

View File

@ -3,6 +3,7 @@
namespace Wallabag\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms a comma-separated list to a proper PHP array.
@ -21,8 +22,6 @@ class StringToListTransformer implements DataTransformerInterface
/**
* Transforms a list to a string.
*
* @param array|null $list
*
* @return string
*/
public function transform($list)
@ -31,14 +30,16 @@ class StringToListTransformer implements DataTransformerInterface
return '';
}
if (!\is_array($list)) {
throw new UnexpectedTypeException($list, 'array');
}
return implode($this->separator, $list);
}
/**
* Transforms a string to a list.
*
* @param string $string
*
* @return array|null
*/
public function reverseTransform($string)
@ -47,6 +48,10 @@ class StringToListTransformer implements DataTransformerInterface
return null;
}
if (!\is_string($string)) {
throw new UnexpectedTypeException($string, 'string');
}
return array_values(array_filter(array_map('trim', explode($this->separator, $string))));
}
}

View File

@ -143,11 +143,8 @@ class ContentProxy
}
try {
// is it already a DateTime?
// (it's inside the try/catch in case of fail to be parse time string)
if (!$date instanceof \DateTime) {
$date = new \DateTime($date);
}
$date = new \DateTime($date);
$entry->setPublishedAt($date);
} catch (\Exception $e) {

View File

@ -98,10 +98,10 @@ class DownloadImages
* - re-saved it (for security reason)
* - return the new local path.
*
* @param int $entryId ID of the entry
* @param string $imagePath Path to the image to retrieve
* @param string $url Url from where the image were found
* @param string $relativePath Relative local path to saved the image
* @param int $entryId ID of the entry
* @param string|null $imagePath Path to the image to retrieve
* @param string $url Url from where the image were found
* @param string $relativePath Relative local path to saved the image
*
* @return string|false Relative url to access the image from the web
*/
@ -136,7 +136,7 @@ class DownloadImages
}
$ext = $this->getExtensionFromResponse($res, $imagePath);
if (false === $res) {
if (false === $ext) {
return false;
}

View File

@ -20,8 +20,8 @@ class Redirect
}
/**
* @param string $url URL to redirect
* @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read
* @param string|null $url URL to redirect
* @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read
*
* @return string
*/

View File

@ -129,7 +129,8 @@ class RuleBasedTagger
private function fixEntry(Entry $entry)
{
$clonedEntry = clone $entry;
$clonedEntry->setReadingTime($entry->getReadingTime() / $entry->getUser()->getConfig()->getReadingSpeed() * 200);
$newReadingTime = (int) ($entry->getReadingTime() / $entry->getUser()->getConfig()->getReadingSpeed() * 200);
$clonedEntry->setReadingTime($newReadingTime);
return $clonedEntry;
}

View File

@ -95,7 +95,7 @@ class PinboardImport extends AbstractImport
'is_archived' => ('no' === $importedEntry['toread']) || $this->markAsRead,
'is_starred' => false,
'created_at' => $importedEntry['time'],
'tags' => explode(' ', (string) $importedEntry['tags']),
'tags' => array_filter(explode(' ', (string) $importedEntry['tags'])),
];
$entry = new Entry($this->user);

View File

@ -52,7 +52,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface
]);
$email = (new Email())
->from(new Address($this->senderEmail, $this->senderName ?? $this->senderEmail))
->from(new Address($this->senderEmail, $this->senderName ?: $this->senderEmail))
->to($user->getEmailAuthRecipient())
->subject($subject)
->text($bodyText)

View File

@ -24,10 +24,10 @@ class Utils
*
* @param string $text
*
* @return float
* @return int
*/
public static function getReadingTime($text)
{
return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200);
return (int) floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200);
}
}

View File

@ -10,7 +10,7 @@
<div class="row">
<div class="tabs-container col s12">
<ul class="tabs">
<ul class="tabs" data-controller="materialize--tabs">
<li class="tab col s12 m6 l3"><a class="active" href="#set1">{{ 'config.tab_menu.settings'|trans }}</a></li>
<li class="tab col s12 m6 l3"><a href="#set2">{{ 'config.tab_menu.feed'|trans }}</a></li>
<li class="tab col s12 m6 l3"><a href="#set3">{{ 'config.tab_menu.user_info'|trans }}</a></li>
@ -21,7 +21,7 @@
</ul>
</div>
<div id="set1" class="col s12">
<div id="set1" class="col s12" data-controller="config">
{{ form_start(form.config) }}
{{ form_errors(form.config) }}
@ -32,7 +32,7 @@
{{ form_label(form.config.items_per_page) }}
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_items_per_page'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_items_per_page'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
@ -47,7 +47,7 @@
</label>
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_display_thumbnails'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_display_thumbnails'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
@ -64,14 +64,14 @@
</p>
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_reading_speed'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_reading_speed'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
</div>
<div class="row">
<div class="input-field col s11">
<div class="input-field col s11" data-controller="materialize--form-select">
{{ form_errors(form.config.action_mark_as_read) }}
{{ form_widget(form.config.action_mark_as_read) }}
{{ form_label(form.config.action_mark_as_read) }}
@ -79,13 +79,13 @@
</div>
<div class="row">
<div class="input-field col s11">
<div class="input-field col s11" data-controller="materialize--form-select">
{{ form_errors(form.config.language) }}
{{ form_widget(form.config.language) }}
{{ form_label(form.config.language) }}
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_language'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_language'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
@ -102,7 +102,7 @@
</p>
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_pocket_consumer_key'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_pocket_consumer_key'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
@ -112,31 +112,31 @@
<div class="input-field col s12">
<h5>{{ 'config.form_settings.android_configuration'|trans }}</h5>
<a href="wallabag://{{ app.user.username }}@{{ wallabag_url }}" class="waves-effect waves-light btn hide-on-large-only">{{ 'config.form_settings.android_instruction'|trans }}</a>
<img class="hide-on-med-and-down jr-qrcode" alt="{{ 'config.otp.app.qrcode_label'|trans }}" data-url="wallabag://{{ app.user.username }}@{{ wallabag_url }}" />
<img data-controller="qrcode" data-qrcode-url-value="wallabag://{{ app.user.username }}@{{ wallabag_url }}" class="hide-on-med-and-down" alt="{{ 'config.otp.app.qrcode_label'|trans }}" />
</div>
</div>
<h5>{{ 'config.tab_menu.article_display'|trans }}</h5>
<div class="row">
<div class="input-field col s5">
<div class="input-field col s5" data-controller="materialize--form-select">
{{ form_errors(form.config.font) }}
{{ form_widget(form.config.font) }}
{{ form_widget(form.config.font, {attr: {'data-config-target': 'font', 'data-action': 'config#updatePreview'}}) }}
{{ form_label(form.config.font) }}
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_font'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_font'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
<div class="input-field col s5">
{{ form_errors(form.config.lineHeight) }}
{{ form_widget(form.config.lineHeight) }}
{{ form_widget(form.config.lineHeight, {attr: {'data-config-target': 'lineHeight', 'data-action': 'config#updatePreview'}}) }}
{{ form_label(form.config.lineHeight, null, {'label_attr': {'class': 'settings-range-label'}}) }}
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_lineheight'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_lineheight'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
@ -145,22 +145,22 @@
<div class="row">
<div class="input-field col s5">
{{ form_errors(form.config.fontsize) }}
{{ form_widget(form.config.fontsize) }}
{{ form_widget(form.config.fontsize, {attr: {'data-config-target': 'fontSize', 'data-action': 'config#updatePreview'}}) }}
{{ form_label(form.config.fontsize, null, {'label_attr': {'class': 'settings-range-label'}}) }}
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_fontsize'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_fontsize'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
<div class="input-field col s5">
{{ form_errors(form.config.maxWidth) }}
{{ form_widget(form.config.maxWidth) }}
{{ form_widget(form.config.maxWidth, {attr: {'data-config-target': 'maxWidth', 'data-action': 'config#updatePreview'}}) }}
{{ form_label(form.config.maxWidth, null, {'label_attr': {'class': 'settings-range-label'}}) }}
</div>
<div class="input-field col s1">
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_maxwidth'|trans }}">
<a href="#" data-controller="materialize--tooltip" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_maxwidth'|trans }}">
<i class="material-icons">live_help</i>
</a>
</div>
@ -168,8 +168,8 @@
<div class="row">
<div class="input-field col s12">
<div id="preview-article">
<article id="preview-content">
<div id="preview-article" data-config-target="previewArticle">
<article id="preview-content" data-config-target="previewContent">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</article>
</div>

View File

@ -15,17 +15,17 @@
<p>{{ 'config.otp.app.two_factor_code_description_2'|trans }}</p>
<p>
<img class="hide-on-med-and-down jr-qrcode" data-url="{{ qr_code|raw }}" />
<img data-controller="qrcode" data-qrcode-url-value="{{ qr_code|raw }}" class="hide-on-med-and-down" />
</p>
<p>
{{ 'config.otp.app.two_factor_code_description_5'|trans }} <pre>{{ secret }}</pre>
</p>
<div data-controller="highlight">
{{ 'config.otp.app.two_factor_code_description_5'|trans }} <pre><code>{{ secret }}</code></pre>
</div>
</li>
<li>
<p>{{ 'config.otp.app.two_factor_code_description_3'|trans }}</p>
<p><pre>{{ backupCodes|join("\n") }}</pre></p>
<div data-controller="highlight"><pre><code>{{ backupCodes|join("\n") }}</code></pre></div>
</li>
<li>
<p>{{ 'config.otp.app.two_factor_code_description_4'|trans }}</p>

View File

@ -18,14 +18,14 @@
<td>{{ 'developer.client_parameter.field_id'|trans }}</td>
<td>
<strong><code>{{ client_id }}</code></strong>
<button class="btn" data-clipboard-text="{{ client_id }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
<button class="btn" data-controller="clipboard" data-clipboard-text="{{ client_id }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
</td>
</tr>
<tr>
<td>{{ 'developer.client_parameter.field_secret'|trans }}</td>
<td>
<strong><code>{{ client_secret }}</code></strong>
<button class="btn" data-clipboard-text="{{ client_secret }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
<button class="btn" data-controller="clipboard" data-clipboard-text="{{ client_secret }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
</td>
</tr>
</table>

View File

@ -12,16 +12,16 @@
<p>{{ 'developer.howto.description.paragraph_2'|trans }}</p>
<p>{{ 'developer.howto.description.paragraph_3'|trans({'%link%': path('developer_create_client')})|raw }}</p>
<p>{{ 'developer.howto.description.paragraph_4'|trans }}</p>
<p>
<div data-controller="highlight">
<pre><code class="language-bash">http POST {{ wallabag_url }}/oauth/v2/token \
grant_type=password \
client_id=12_5um6nz50ceg4088c0840wwc0kgg44g00kk84og044ggkscso0k \
client_secret=3qd12zpeaxes8cwg8c0404g888co4wo8kc4gcw0occww8cgw4k \
username=yourUsername \
password=yourPassw0rd</code></pre>
</p>
</div>
<p>{{ 'developer.howto.description.paragraph_5'|trans }}</p>
<p>
<div data-controller="highlight">
<pre><code class="language-bash">HTTP/1.1 200 OK
Cache-Control: no-store, private
Connection: close
@ -39,12 +39,12 @@ X-Powered-By: PHP/5.5.9-1ubuntu4.13
"scope": null,
"token_type": "bearer"
}</code></pre>
</p>
</div>
<p>{{ 'developer.howto.description.paragraph_6'|trans }}</p>
<p>
<div data-controller="highlight">
<pre><code class="language-bash">http GET {{ wallabag_url }}/api/entries.json \
"Authorization:Bearer ZWFjNjA3ZWMwYWVmYzRkYTBlMmQ3NTllYmVhOGJiZDE0ZTg1NjE4MjczOTVlNzM0ZTRlMWQ0MmRlMmYwNTk5Mw"</code></pre>
</p>
</div>
<p>{{ 'developer.howto.description.paragraph_7'|trans }}</p>
<p>{{ 'developer.howto.description.paragraph_8'|trans({'%link%': path('nelmio_api_doc.swagger_ui')})|raw }}</p>
<p><a href="{{ path('developer') }}" class="waves-effect waves-light grey btn">{{ 'developer.howto.back'|trans }}</a></p>

View File

@ -25,7 +25,7 @@
<h4>{{ 'developer.existing_clients.title'|trans }}</h4>
{% if clients %}
<ul class="collapsible" data-collapsible="expandable">
<ul class="collapsible" data-controller="materialize--collapsible" data-materialize--collapsible-accordion-value="false">
{% for client in clients %}
<li>
<div class="collapsible-header">{{ client.name }} - #{{ client.id }}</div>
@ -35,14 +35,14 @@
<td>{{ 'developer.existing_clients.field_id'|trans }}</td>
<td>
<strong><code>{{ client.clientId }}</code></strong>
<button class="btn" data-clipboard-text="{{ client.clientId }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
<button class="btn" data-controller="clipboard" data-clipboard-text="{{ client.clientId }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
</td>
</tr>
<tr>
<td>{{ 'developer.existing_clients.field_secret'|trans }}</td>
<td>
<strong><code>{{ client.secret }}</code></strong>
<button class="btn" data-clipboard-text="{{ client.secret }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
<button class="btn" data-controller="clipboard" data-clipboard-text="{{ client.secret }}">{{ 'developer.client.copy_to_clipboard'|trans }}</button>
</td>
</tr>
<tr>

View File

@ -1,3 +1,3 @@
<label class="entry-checkbox">
<input type="checkbox" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
<input type="checkbox" class="entry-checkbox-input" name="entry-checkbox[]" value="{{ entry.id }}" data-batch-edit-target="item" />
</label>

View File

@ -24,21 +24,21 @@
{% block nav_panel_extra_actions %}
{% if active_route %}
<li>
<a class="waves-effect tooltipped js-random-action" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.random_entry'|trans }}" href="{{ path('random_entry', {'type': active_route}) }}">
<a class="waves-effect js-random-action" data-controller="materialize--tooltip" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.random_entry'|trans }}" href="{{ path('random_entry', {'type': active_route}) }}">
<i class="material-icons">casino</i>
</a>
</li>
{% endif %}
{% if has_filters %}
<li class="button-filters">
<a class="nav-panel-menu sidenav-trigger tooltipped js-filters-action" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.filter_entries'|trans }}" href="#" data-target="filters">
<a class="nav-panel-menu sidenav-trigger js-filters-action" data-controller="materialize--tooltip" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.filter_entries'|trans }}" href="#" data-target="filters">
<i class="material-icons">filter_list</i>
</a>
</li>
{% endif %}
{% if has_exports %}
<li class="button-export">
<a class="nav-panel-menu sidenav-trigger tooltipped js-export-action" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.export'|trans }}" href="#" data-target="export">
<a class="nav-panel-menu sidenav-trigger js-export-action" data-controller="materialize--tooltip" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.export'|trans }}" href="#" data-target="export">
<i class="material-icons">file_download</i>
</a>
</li>
@ -53,15 +53,15 @@
{% if current_route == 'homepage' %}
{% set current_route = 'unread' %}
{% endif %}
<form name="form_mass_action" action="{{ path('mass_action', {redirect: current_path}) }}" method="post">
<div class="results">
<form name="form_mass_action" action="{{ path('mass_action', {redirect: current_path}) }}" method="post" data-controller="batch-edit entries-navigation">
<div class="results" data-entries-navigation-target="paginationWrapper">
<div class="nb-results">
{{ 'entry.list.number_on_the_page'|trans({'%count%': entries.count}) }}
{% if entries.count > 0 %}
<a class="results-item" href="{{ path('switch_view_mode', {redirect: current_path}) }}"><i class="material-icons">{% if list_mode == 0 %}view_list{% else %}view_module{% endif %}</i></a>
{% endif %}
{% if entries.count > 0 and is_granted('EDIT_ENTRIES') %}
<label for="mass-action-inputs-displayed" class="mass-action-toggle results-item tooltipped" data-position="right" data-delay="50" data-tooltip="{{ 'entry.list.toggle_mass_action'|trans }}"><i class="material-icons">library_add_check</i></label>
<label for="mass-action-inputs-displayed" class="mass-action-toggle results-item" data-controller="materialize--tooltip" data-position="right" data-delay="50" data-tooltip="{{ 'entry.list.toggle_mass_action'|trans }}"><i class="material-icons">library_add_check</i></label>
{% endif %}
{% if app.user.config.feedToken %}
{% include "Entry/_feed_link.html.twig" %}
@ -77,22 +77,22 @@
<input id="mass-action-inputs-displayed" class="toggle-checkbox" type="checkbox" />
<div class="mass-action">
<div class="mass-action-group">
<input type="checkbox" class="entry-checkbox-input" data-toggle="[data-js='entry-checkbox']" data-js="checkboxes-toggle" />
<input type="checkbox" class="entry-checkbox-input" data-action="batch-edit#toggleSelection" />
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-read" title="{{ 'entry.list.toogle_as_read'|trans }}"><i class="material-icons">done</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-star" title="{{ 'entry.list.toogle_as_star'|trans }}" ><i class="material-icons">star</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="delete" onclick="return confirm('{{ 'entry.confirm.delete_entries'|trans|escape('js') }}')" title="{{ 'entry.list.delete'|trans }}"><i class="material-icons">delete</i></button>
</div>
<div class="mass-action-tags">
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" name="tag" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></button>
<input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" />
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" name="tag" title="{{ 'entry.list.add_tags'|trans }}" data-batch-edit-target="tagAction"><i class="material-icons">label</i></button>
<input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" data-action="keydown.enter->batch-edit#tagSelection:prevent:stop" />
</div>
</div>
<ol class="entries {% if list_mode == 1 %}collection{% else %}row entries-row data{% endif %}">
{% for entry in entries %}
<li id="entry-{{ entry.id|e }}" class="{% if list_mode != 0 %}col collection-item{% endif %} s12" data-entry-id="{{ entry.id|e }}" data-test="entry">
<li id="entry-{{ entry.id|e }}" class="{% if list_mode != 0 %}col collection-item{% endif %} s12" data-entry-id="{{ entry.id|e }}" data-test="entry" data-entries-navigation-target="card">
{% if list_mode == 1 %}
{% include "Entry/_card_list.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %}
{% elseif not entry.previewPicture is null and entry.mimetype starts with 'image/' %}
@ -114,7 +114,7 @@
<!-- Export -->
{% if has_exports and is_granted('EXPORT_ENTRIES') %}
<div id="export" class="sidenav" data-edge="right">
<div id="export" class="sidenav" data-controller="materialize--sidenav" data-materialize--sidenav-edge-value="right">
{% set current_tag = null %}
{% if tag is defined %}
{% set current_tag = tag.slug %}
@ -140,7 +140,7 @@
<!-- Filters -->
{% if has_filters %}
<div id="filters" class="sidenav" data-edge="right">
<div id="filters" class="sidenav" data-controller="materialize--sidenav" data-materialize--sidenav-edge-value="right">
<form action="{{ path('all') }}">
<h4 class="center">{{ 'entry.filters.title'|trans }}</h4>

View File

@ -8,9 +8,9 @@
{% block menu %}
<div class="progress">
<div class="determinate"></div>
<div class="determinate" data-controller="scroll-indicator" data-action="scroll@window->scroll-indicator#updateWidth"></div>
</div>
<nav class="hide-on-large-only js-entry-nav-top">
<nav class="hide-on-large-only" data-controller="sticky-nav" data-action="materialize--fab:click@window->sticky-nav#toggle">
<div class="nav-panel-item cyan darken-1">
<ul>
<li>
@ -42,7 +42,7 @@
</ul>
</div>
</nav>
<ul id="slide-out" class="left-bar collapsible sidenav sidenav-fixed reader-mode" data-collapsible="accordion">
<ul id="slide-out" class="left-bar collapsible sidenav sidenav-fixed reader-mode" data-controller="materialize--sidenav materialize--collapsible leftbar">
<li class="bold border-bottom hide-on-med-and-down">
<a class="waves-effect collapsible-header" href="{{ path('homepage') }}">
<i class="material-icons small">arrow_back</i>
@ -52,7 +52,7 @@
</li>
<li class="bold border-bottom hide-on-med-and-down">
<a class="waves-effect collapsible-header original" href="{{ entry.url|e }}" target="_blank" rel="noopener">
<a class="waves-effect collapsible-header original" href="{{ entry.url|e }}" target="_blank" rel="noopener" data-shortcuts-target="openOriginal">
<i class="material-icons small">link</i>
<span>{{ 'entry.view.left_menu.view_original_article'|trans }}</span>
</a>
@ -76,7 +76,7 @@
{% if is_granted('ARCHIVE', entry) %}
<li class="bold hide-on-med-and-down">
<a class="waves-effect collapsible-header markasread" title="{{ mark_as_read_label|trans }}" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" id="markAsRead">
<a class="waves-effect collapsible-header markasread" title="{{ mark_as_read_label|trans }}" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" id="markAsRead" data-shortcuts-target="markAsRead">
<i class="material-icons small">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
<span>{{ mark_as_read_label|trans }}</span>
</a>
@ -86,7 +86,7 @@
{% if is_granted('STAR', entry) %}
<li class="bold hide-on-med-and-down">
<a class="waves-effect collapsible-header favorite" title="{{ 'entry.view.left_menu.set_as_starred'|trans }}" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" id="setFav">
<a class="waves-effect collapsible-header favorite" title="{{ 'entry.view.left_menu.set_as_starred'|trans }}" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" id="setFav" data-shortcuts-target="markAsFavorite">
<i class="material-icons small">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
<span>{{ 'entry.view.left_menu.set_as_starred'|trans }}</span>
</a>
@ -96,7 +96,7 @@
{% if is_granted('DELETE', entry) %}
<li class="bold border-bottom">
<a class="waves-effect collapsible-header delete" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.delete'|trans }}" href="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}">
<a class="waves-effect collapsible-header delete" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.delete'|trans }}" href="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" data-shortcuts-target="deleteEntry">
<i class="material-icons small">delete</i>
<span>{{ 'entry.view.left_menu.delete'|trans }}</span>
</a>
@ -105,7 +105,7 @@
{% endif %}
<li class="bold border-bottom">
<a class="waves-effect collapsible-header" id="nav-btn-add-tag">
<a class="waves-effect collapsible-header" id="nav-btn-add-tag" data-action="click->materialize--sidenav#close click->leftbar#toggleAddTagForm">
<i class="material-icons small">label_outline</i>
<span>{{ 'entry.view.left_menu.add_a_tag'|trans }}</span>
</a>
@ -119,19 +119,19 @@
</a>
<ul class="collapsible-body">
<li>
<a href="#" class="js-theme-toggle" data-theme="light">
<a href="#" data-action="click->dark-theme#useLight:prevent">
<i class="theme-toggle-icon material-icons tiny">brightness_high</i>
<span>{{ 'entry.view.left_menu.theme_toggle_light'|trans }}</span>
</a>
</li>
<li>
<a href="#" class="js-theme-toggle" data-theme="dark">
<a href="#" data-action="click->dark-theme#useDark:prevent">
<i class="theme-toggle-icon material-icons tiny">brightness_low</i>
<span>{{ 'entry.view.left_menu.theme_toggle_dark'|trans }}</span>
</a>
</li>
<li>
<a href="#" class="js-theme-toggle" data-theme="auto">
<a href="#" data-action="click->dark-theme#useAuto:prevent">
<i class="theme-toggle-icon material-icons tiny">brightness_auto</i>
<span>{{ 'entry.view.left_menu.theme_toggle_auto'|trans }}</span>
</a>
@ -262,7 +262,7 @@
{% endblock %}
{% block content %}
<div id="article" class="article">
<div id="article" class="article" data-controller="scroll-storage" data-scroll-storage-entry-id-value="{{ entry.id }}" data-action="scroll@window->scroll-storage#saveScroll">
<header class="mbm">
<h1>
<span{% if entry.language is defined and entry.language is not null %} lang="{{ entry.getHTMLLanguage() }}"{% endif %}>{{ entry.title|striptags|default('entry.default_title'|trans)|raw }}</span>
@ -320,17 +320,25 @@
</div>
{% if is_granted('TAG', entry) %}
<div class="input-field nav-panel-add-tag" style="display: none">
<div class="input-field nav-panel-add-tag hidden" data-controller="add-tag" data-action="leftbar:toggleAddTagForm@window->add-tag#toggle">
{{ render(controller('Wallabag\\Controller\\TagController::addTagFormAction', {'id': entry.id})) }}
</div>
{% endif %}
</aside>
<article{% if entry.language is defined and entry.language is not null %} lang="{{ entry.getHTMLLanguage() }}"{% endif %}>
<article
{% if entry.language is defined and entry.language is not null %} lang="{{ entry.getHTMLLanguage() }}"{% endif %}
data-controller="highlight annotations"
data-annotations-entry-id-value="{{ entry.id }}"
data-annotations-create-url-value="{{ path('annotations_post_annotation', {'entry': entry.id}) }}"
data-annotations-update-url-value="{{ path('annotations_put_annotation', {'annotation': 'idAnnotation'}) }}"
data-annotations-destroy-url-value="{{ path('annotations_delete_annotation', {'annotation': 'idAnnotation'}) }}"
data-annotations-search-url-value="{{ path('annotations_get_annotations', {'entry': entry.id}) }}"
>
{{ entry.content|raw }}
</article>
<div class="fixed-action-btn js-fixed-action-btn horizontal click-to-toggle hide-on-large-only">
<div class="fixed-action-btn horizontal click-to-toggle hide-on-large-only" data-controller="materialize--fab" data-action="scroll@window->materialize--fab#autoDisplay click->materialize--fab#click">
<a class="btn-floating btn-large" data-toggle="actions">
<i class="material-icons">menu</i>
</a>
@ -347,19 +355,6 @@
</ul>
</div>
</div>
<script id="annotationroutes" type="application/json">
{
"prefix": "",
"urls": {
"create": "{{ path('annotations_post_annotation', {'entry': entry.id}) }}",
"update": "{{ path('annotations_put_annotation', {'annotation': 'idAnnotation'}) }}",
"destroy": "{{ path('annotations_delete_annotation', {'annotation': 'idAnnotation'}) }}",
"search": "{{ path('annotations_get_annotations', {'entry': entry.id}) }}"
},
"entryId": "{{ entry.id }}"
}</script>
{% endblock %}
{% block footer %}

View File

@ -1,4 +1,4 @@
<form class="input-field nav-panel-item nav-panel-add" style="display: none" name="entry" method="post" action="{{ path('new_entry') }}">
<form class="input-field nav-panel-item nav-panel-add" style="display: none" name="entry" method="post" action="{{ path('new_entry') }}" data-topbar-target="addUrl" data-action="topbar#submittingUrl">
{% if form_errors(form) %}
<span class="black-text">{{ form_errors(form) }}</span>
{% endif %}
@ -8,8 +8,8 @@
<span class="black-text">{{ form_errors(form.url) }}</span>
{% endif %}
{{ form_widget(form.url, {'attr': {'autocomplete': 'off', 'placeholder': 'entry.new.placeholder'}}) }}
<i class="material-icons close" aria-label="clear" role="button"></i>
{{ form_widget(form.url, {'attr': {'autocomplete': 'off', 'placeholder': 'entry.new.placeholder', 'data-topbar-target': 'addUrlInput'}}) }}
<i class="material-icons close" aria-label="clear" role="button" data-action="click->topbar#showActions" data-shortcuts-target="showActions"></i>
{{ form_rest(form) }}
</form>

View File

@ -1,4 +1,4 @@
<form class="input-field nav-panel-item nav-panel-search" style="display: none" name="search" method="GET" action="{{ path('search') }}">
<form class="input-field nav-panel-item nav-panel-search" style="display: none" name="search" method="GET" action="{{ path('search') }}" data-topbar-target="search">
{% if form_errors(form) %}
<span class="black-text">{{ form_errors(form) }}</span>
{% endif %}
@ -10,8 +10,8 @@
<input type="hidden" name="currentRoute" value="{{ currentRoute }}" />
{{ form_widget(form.term, {'attr': {'autocomplete': 'off', 'placeholder': 'entry.search.placeholder'}}) }}
<i class="material-icons close" aria-label="clear" role="button"></i>
{{ form_widget(form.term, {'attr': {'autocomplete': 'off', 'placeholder': 'entry.search.placeholder', 'data-topbar-target': 'searchInput'}}) }}
<i class="material-icons close" aria-label="clear" role="button" data-action="click->topbar#showActions" data-shortcuts-target="showActions"></i>
{{ form_rest(form) }}
</form>

View File

@ -1,9 +1,9 @@
{% if nbRedisMessages is defined and nbRedisMessages > 0 %}
<p class="material-toast hidden">Messages in queue: {{ nbRedisMessages }}</p>
<p class="hidden" data-controller="materialize--toast">Messages in queue: {{ nbRedisMessages }}</p>
{% endif %}
{% if nbRabbitMessages is defined and nbRabbitMessages > 0 %}
<p class="material-toast hidden">Messages in queue: {{ nbRabbitMessages }}</p>
<p class="hidden" data-controller="materialize--toast">Messages in queue: {{ nbRabbitMessages }}</p>
{% endif %}
{% if redisNotInstalled is defined and redisNotInstalled %}

View File

@ -10,7 +10,7 @@
<div class="row">
<div class="tabs-container col s12">
<ul class="tabs">
<ul class="tabs" data-controller="materialize--tabs">
<li class="tab col s12 m6 l3"><a class="active" href="#set1">{{ 'about.top_menu.who_behind_wallabag'|trans }}</a></li>
<li class="tab col s12 m6 l3"><a href="#set2">{{ 'about.top_menu.getting_help'|trans }}</a></li>
<li class="tab col s12 m6 l3"><a href="#set3">{{ 'about.top_menu.helping'|trans }}</a></li>

View File

@ -10,7 +10,7 @@
<div class="row">
<div class="tabs-container col s12">
<ul class="tabs">
<ul class="tabs" data-controller="materialize--tabs">
<li class="tab col s12 m6 l3"><a class="active" href="#set1">{{ 'howto.tab_menu.add_link'|trans }}</a></li>
<li class="tab col s12 m6 l3"><a href="#set2">{{ 'howto.tab_menu.shortcuts'|trans }}</a></li>
</ul>

View File

@ -7,7 +7,7 @@
<span class="black-text">{{ form_errors(form.label) }}</span>
{% endif %}
{{ form_widget(form.label, {'attr': {'autocomplete': 'off'}}) }}
{{ form_widget(form.label, {'attr': {'autocomplete': 'off', 'data-add-tag-target': 'input'}}) }}
{{ form_widget(form.add, {'attr': {'class': 'btn waves-effect waves-light tags-add-form-submit'}}) }}
{{ form_widget(form._token) }}

View File

@ -19,18 +19,19 @@
{% for tagWithNbEntries in allTagsWithNbEntries %}
{% set tag = tagWithNbEntries.tag %}
{% set nbEntries = tagWithNbEntries.nbEntries %}
<li title="{{ tag.label }} ({{ nbEntries }})" id="tag-{{ tag.id }}" class="chip">
<a href="{{ path('tag_entries', {'slug': tag.slug}) }}" class="card-tag-link" data-handle="tag-link">
<li title="{{ tag.label }} ({{ nbEntries }})" id="tag-{{ tag.id }}" class="chip" data-controller="tag">
<a href="{{ path('tag_entries', {'slug': tag.slug}) }}" class="card-tag-link" data-tag-target="link">
{{ tag.label }}&nbsp;({{ nbEntries }})
</a>
{% if renameForms is defined and renameForms[tag.id] is defined and is_granted('EDIT', tag) %}
<form class="card-tag-form hidden" data-handle="tag-rename-form" action="{{ path('tag_rename', {'slug': tag.slug, redirect: current_path}) }}" method="POST">
{{ form_widget(renameForms[tag.id].label, {'attr': {'value': tag.label}}) }}
<form class="card-tag-form hidden" data-tag-target="form" action="{{ path('tag_rename', {'slug': tag.slug, redirect: current_path}) }}" method="POST">
{{ form_widget(renameForms[tag.id].label, {'attr': {'value': tag.label, 'data-tag-target': 'input'}}) }}
{{ form_rest(renameForms[tag.id]) }}
<button type="submit"><i class="material-icons">done</i></button>
</form>
<a class="card-tag-icon card-tag-rename" data-handler="tag-rename" href="javascript:void(0);">
<button type="button" data-tag-target="edit" data-action="tag#showForm">
<i class="material-icons">mode_edit</i>
</a>
</button>
{% endif %}
{% if is_granted('DELETE', tag) %}
<a id="delete-{{ tag.slug }}" href="{{ path('tag_delete', {'slug': tag.slug, redirect: current_path}) }}" class="card-tag-icon card-tag-delete" onclick="return confirm('{{ 'tag.confirm.delete'|trans({'%name%': tag.label})|escape('js') }}')">

View File

@ -48,16 +48,16 @@
</div>
</div>
<div class="row">
<div class="row" data-controller="fake-radio">
<div class="input-field col s12">
<label for="{{ edit_form.emailTwoFactor.vars.id }}">
{{ form_widget(edit_form.emailTwoFactor) }}
{{ form_widget(edit_form.emailTwoFactor, {attr: {'data-fake-radio-target': 'emailTwoFactor', 'data-action': 'fake-radio#uncheckGoogle'}}) }}
<span>{{ edit_form.emailTwoFactor.vars.label|trans }}</span>
</label>
</div>
<div class="input-field col s12">
<label for="{{ edit_form.googleTwoFactor.vars.id }}">
{{ form_widget(edit_form.googleTwoFactor) }}
{{ form_widget(edit_form.googleTwoFactor, {attr: {'data-fake-radio-target': 'googleTwoFactor', 'data-action': 'fake-radio#uncheckEmail'}}) }}
<span>{{ edit_form.googleTwoFactor.vars.label|trans }}</span>
</label>
</div>

View File

@ -4,7 +4,7 @@
<!--[if lte IE 7]><html class="no-js ie7 ie67 ie678"{% if lang is not empty %} lang="{{ lang }}"{% endif %}><![endif]-->
<!--[if IE 8]><html class="no-js ie8 ie678"{% if lang is not empty %} lang="{{ lang }}"{% endif %}><![endif]-->
<!--[if gt IE 8]><html class="no-js"{% if lang is not empty %} lang="{{ lang }}"{% endif %}><![endif]-->
<html{% if lang is not empty %} class="{{ theme_class() }}" lang="{{ lang }}"{% endif %}>
<html{% if lang is not empty %} lang="{{ lang }}"{% endif %} class="{{ theme_class() }}" data-controller="dark-theme shortcuts" data-shortcuts-entries-navigation-outlet="[data-controller~='entries-navigation']">
<head>
{% block head %}
<meta name="viewport" content="initial-scale=1.0">

View File

@ -11,7 +11,7 @@
<div class="row">
<div class="div_tabs col s12">
<ul class="tabs">
<ul class="tabs" data-controller="materialize--tabs">
{% for section in sections|craue_sortSections %}
<li class="tab col s12 m6 l3"><a href="#set-{{ section }}">{{ section|trans({}, 'CraueConfigBundle') }}</a></li>
{% endfor %}

View File

@ -5,11 +5,11 @@
<div class="card-content">
{% if error %}
<p class="material-toast hidden">{{ error.messageKey|trans(error.messageData, 'security') }}</p>
<p class="hidden" data-controller="materialize--toast">{{ error.messageKey|trans(error.messageData, 'security') }}</p>
{% endif %}
{% for flash_message in app.session.flashbag.get('notice') %}
<p class="material-toast hidden">{{ flash_message }}</p>
<p class="hidden" data-controller="materialize--toast">{{ flash_message }}</p>
{% endfor %}
<div class="row">

View File

@ -35,7 +35,7 @@
{% block messages %}
{% for flash_message in app.session.flashbag.get('notice') %}
<p class="material-toast hidden">{{ flash_message|trans }}</p>
<p class="hidden" data-controller="materialize--toast">{{ flash_message|trans }}</p>
{% endfor %}
{% endblock %}
@ -43,7 +43,7 @@
{% block menu %}
<nav class="cyan darken-1">
<ul id="slide-out" class="left-bar sidenav sidenav-fixed">
<ul id="slide-out" class="left-bar sidenav sidenav-fixed" data-controller="materialize--sidenav">
{% block logo %}
<li class="logo border-bottom">
{% if is_granted('LIST_ENTRIES') %}
@ -79,8 +79,8 @@
</li>
{% endif %}
</ul>
<div class="nav-panels">
<div class="nav-panel-actions nav-panel-item">
<div class="nav-panels" data-controller="topbar">
<div class="nav-panel-actions nav-panel-item" data-topbar-target="actions">
<div class="nav-panel-top">
<a href="#" data-target="slide-out" class="nav-panel-menu sidenav-trigger"><i class="material-icons">menu</i></a>
<h1 class="left action">
@ -90,18 +90,18 @@
</div>
<ul class="input-field nav-panel-buttom">
<li class="bold toggle-add-url-container">
<a class="waves-effect tooltipped toggle-add-url" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.add_new_entry'|trans }}" href="{{ path('new') }}" id="nav-btn-add">
<a class="waves-effect toggle-add-url" data-controller="materialize--tooltip" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.add_new_entry'|trans }}" href="{{ path('new') }}" data-action="topbar#showAddUrl:prevent:stop" data-shortcuts-target="showAddUrl">
<i class="material-icons">add</i>
</a>
</li>
<li>
<a class="waves-effect tooltipped" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.search'|trans }}" href="javascript: void(null);" id="nav-btn-search">
<a class="waves-effect" data-controller="materialize--tooltip" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.search'|trans }}" href="javascript: void(null);" data-action="topbar#showSearch:prevent:stop" data-shortcuts-target="showSearch">
<i class="material-icons">search</i>
</a>
</li>
{% block nav_panel_extra_actions '' %}
<li class="bold">
<a class="wave-effect tooltipped dropdown-trigger" data-covertrigger="false" data-constrainwidth="false" data-target="dropdown-account" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.account'|trans }}" href="#" id="news_menu">
<a class="wave-effect dropdown-trigger" data-controller="materialize--dropdown materialize--tooltip" data-target="dropdown-account" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.account'|trans }}" href="#" id="news_menu">
<i class="material-icons" id="news_link">account_circle</i>
</a>
</li>
@ -134,19 +134,19 @@
<li class="divider"></li>
{% endif %}
<li>
<a href="#" class="js-theme-toggle" data-theme="light">
<a href="#" data-action="click->dark-theme#useLight:prevent">
<i class="theme-toggle-icon material-icons tiny">brightness_high</i>
<span>{{ 'menu.left.theme_toggle_light'|trans }}</span>
</a>
</li>
<li>
<a href="#" class="js-theme-toggle" data-theme="dark">
<a href="#" data-action="click->dark-theme#useDark:prevent">
<i class="theme-toggle-icon material-icons tiny">brightness_low</i>
<span>{{ 'menu.left.theme_toggle_dark'|trans }}</span>
</a>
</li>
<li>
<a href="#" class="js-theme-toggle" data-theme="auto">
<a href="#" data-action="click->dark-theme#useAuto:prevent">
<i class="theme-toggle-icon material-icons tiny">brightness_auto</i>
<span>{{ 'menu.left.theme_toggle_auto'|trans }}</span>
</a>

View File

@ -43,8 +43,6 @@ class WallabagRestControllerTest extends WallabagApiTestCase
if (!$client->getContainer()->getParameter('fosuser_registration')) {
$this->markTestSkipped('fosuser_registration is not enabled.');
return;
}
$client->getContainer()->get(Config::class)->set('api_user_registration', 1);

View File

@ -11,8 +11,6 @@ class ExportControllerTest extends WallabagTestCase
{
private $adminEntry;
private $bobEntry;
private $sameDomainEntry;
private $sameDomainEntry2;
public function testLogin()
{

View File

@ -88,8 +88,6 @@ class SecurityControllerTest extends WallabagTestCase
if (!$client->getContainer()->getParameter('fosuser_registration')) {
$this->markTestSkipped('fosuser_registration is not enabled.');
return;
}
$client->followRedirects();

View File

@ -55,7 +55,7 @@ class ContentProxyTest extends TestCase
$this->assertEmpty($entry->getPreviewPicture());
$this->assertEmpty($entry->getMimetype());
$this->assertEmpty($entry->getLanguage());
$this->assertSame(0.0, $entry->getReadingTime());
$this->assertSame(0, $entry->getReadingTime());
$this->assertNull($entry->getDomainName());
$this->assertTrue($entry->isNotParsed());
}
@ -95,7 +95,7 @@ class ContentProxyTest extends TestCase
$this->assertEmpty($entry->getPreviewPicture());
$this->assertEmpty($entry->getMimetype());
$this->assertEmpty($entry->getLanguage());
$this->assertSame(0.0, $entry->getReadingTime());
$this->assertSame(0, $entry->getReadingTime());
$this->assertSame('0.0.0.0', $entry->getDomainName());
$this->assertTrue($entry->isNotParsed());
}
@ -138,7 +138,7 @@ class ContentProxyTest extends TestCase
$this->assertEmpty($entry->getLanguage());
$this->assertEmpty($entry->getHttpStatus());
$this->assertEmpty($entry->getMimetype());
$this->assertSame(0.0, $entry->getReadingTime());
$this->assertSame(0, $entry->getReadingTime());
$this->assertSame('domain.io', $entry->getDomainName());
$this->assertTrue($entry->isNotParsed());
}
@ -184,7 +184,7 @@ class ContentProxyTest extends TestCase
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame('200', $entry->getHttpStatus());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertFalse($entry->isNotParsed());
}
@ -230,7 +230,7 @@ class ContentProxyTest extends TestCase
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame('200', $entry->getHttpStatus());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertFalse($entry->isNotParsed());
}
@ -275,7 +275,7 @@ class ContentProxyTest extends TestCase
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame('200', $entry->getHttpStatus());
$this->assertSame(0.0, $entry->getReadingTime());
$this->assertSame(0, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertFalse($entry->isNotParsed());
}
@ -320,7 +320,7 @@ class ContentProxyTest extends TestCase
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame('200', $entry->getHttpStatus());
$this->assertSame(0.0, $entry->getReadingTime());
$this->assertSame(0, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertFalse($entry->isNotParsed());
}
@ -368,7 +368,7 @@ class ContentProxyTest extends TestCase
$this->assertSame('text/html', $entry->getMimetype());
$this->assertNull($entry->getLanguage());
$this->assertSame('200', $entry->getHttpStatus());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertFalse($entry->isNotParsed());
}
@ -422,7 +422,7 @@ class ContentProxyTest extends TestCase
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame('200', $entry->getHttpStatus());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertFalse($entry->isNotParsed());
}
@ -461,7 +461,7 @@ class ContentProxyTest extends TestCase
$this->assertStringContainsString('content', $entry->getContent());
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertSame('24/03/2014', $entry->getPublishedAt()->format('d/m/Y'));
$this->assertContains('Jeremy', $entry->getPublishedBy());
@ -505,7 +505,7 @@ class ContentProxyTest extends TestCase
$this->assertStringContainsString('content', $entry->getContent());
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertSame('08/09/2016', $entry->getPublishedAt()->format('d/m/Y'));
$this->assertFalse($entry->isNotParsed());
@ -545,7 +545,7 @@ class ContentProxyTest extends TestCase
$this->assertStringContainsString('content', $entry->getContent());
$this->assertSame('text/html', $entry->getMimetype());
$this->assertSame('fr', $entry->getLanguage());
$this->assertSame(4.0, $entry->getReadingTime());
$this->assertSame(4, $entry->getReadingTime());
$this->assertSame('1.1.1.1', $entry->getDomainName());
$this->assertNull($entry->getPublishedAt());
$this->assertFalse($entry->isNotParsed());
@ -883,42 +883,42 @@ class ContentProxyTest extends TestCase
*/
$this->markTestSkipped('Encoding issue in PHP >= 8.1');
// See http://graphemica.com for more info about the characters
// '😻<F09F98BB>z' (U+1F63B or F09F98BB; U+2124 or E284A4; invalid character 81; U+007A or 7A) in hexadecimal and UTF-8
// 0x81 is not a valid character for UTF16, UTF8 and WINDOWS-1252
$actualTitle = $this->hexToStr('F09F98BB' . 'E284A4' . '81' . '7A');
$tagger = $this->getTaggerMock();
$tagger->expects($this->once())
->method('tag');
$ruleBasedIgnoreOriginProcessor = $this->getRuleBasedIgnoreOriginProcessorMock();
$graby = $this->getMockBuilder(Graby::class)
->onlyMethods(['fetchContent'])
->disableOriginalConstructor()
->getMock();
$graby->expects($this->any())
->method('fetchContent')
->willReturn([
'html' => false,
'title' => $actualTitle,
'url' => '',
'headers' => [
'content-type' => 'application/pdf',
],
'language' => '',
]);
$proxy = new ContentProxy($graby, $tagger, $ruleBasedIgnoreOriginProcessor, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage);
$entry = new Entry(new User());
$proxy->updateEntry($entry, 'http://0.0.0.0');
// '😻z' (U+1F63B or F09F98BB; U+2124 or E284A4; U+007A or 7A) in hexadecimal and UTF-8
// the 0x81 (represented by <20>) is invalid for UTF16, UTF8 and WINDOWS-1252 and is removed
$expectedTitle = 'F09F98BB' . 'E284A4' . '7A';
$this->assertSame($expectedTitle, $this->strToHex($entry->getTitle()));
// // See http://graphemica.com for more info about the characters
// // '😻<F09F98BB>z' (U+1F63B or F09F98BB; U+2124 or E284A4; invalid character 81; U+007A or 7A) in hexadecimal and UTF-8
// // 0x81 is not a valid character for UTF16, UTF8 and WINDOWS-1252
// $actualTitle = $this->hexToStr('F09F98BB' . 'E284A4' . '81' . '7A');
//
// $tagger = $this->getTaggerMock();
// $tagger->expects($this->once())
// ->method('tag');
//
// $ruleBasedIgnoreOriginProcessor = $this->getRuleBasedIgnoreOriginProcessorMock();
//
// $graby = $this->getMockBuilder(Graby::class)
// ->onlyMethods(['fetchContent'])
// ->disableOriginalConstructor()
// ->getMock();
//
// $graby->expects($this->any())
// ->method('fetchContent')
// ->willReturn([
// 'html' => false,
// 'title' => $actualTitle,
// 'url' => '',
// 'headers' => [
// 'content-type' => 'application/pdf',
// ],
// 'language' => '',
// ]);
//
// $proxy = new ContentProxy($graby, $tagger, $ruleBasedIgnoreOriginProcessor, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage);
// $entry = new Entry(new User());
// $proxy->updateEntry($entry, 'http://0.0.0.0');
//
// // '😻z' (U+1F63B or F09F98BB; U+2124 or E284A4; U+007A or 7A) in hexadecimal and UTF-8
// // the 0x81 (represented by <20>) is invalid for UTF16, UTF8 and WINDOWS-1252 and is removed
// $expectedTitle = 'F09F98BB' . 'E284A4' . '7A';
// $this->assertSame($expectedTitle, $this->strToHex($entry->getTitle()));
}
/**

View File

@ -13,7 +13,7 @@ class UtilsTest extends TestCase
*/
public function testCorrectWordsCountForDifferentLanguages($filename, $text, $expectedCount)
{
static::assertSame((float) $expectedCount, Utils::getReadingTime($text), 'Reading time for: ' . $filename);
static::assertSame($expectedCount, Utils::getReadingTime($text), 'Reading time for: ' . $filename);
}
public function examples()
@ -30,7 +30,7 @@ class UtilsTest extends TestCase
$examples[] = [
$file->getRelativePathname(),
$match[1], // content
$match[2], // reading time
(int) $match[2], // reading time
];
}

View File

@ -10,6 +10,7 @@ Encore
.addEntry('main', './assets/index.js')
.addEntry('public', './assets/share.js')
.splitEntryChunks()
.enableStimulusBridge('./assets/controllers.json')
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
@ -21,7 +22,6 @@ Encore
config.corejs = '3.23';
})
.enableSassLoader()
.enablePostCssLoader()
.autoProvidejQuery();
.enablePostCssLoader();
module.exports = Encore.getWebpackConfig();

View File

@ -911,6 +911,16 @@
resolved "https://registry.yarnpkg.com/@fontsource/oswald/-/oswald-5.2.5.tgz#01ef5b61fae44542eb22330255d32b728ee45b0c"
integrity sha512-Sw8ayEYCoOzG2ISw5HaX3d5ILt3OEG2VFX2nzHaGywYD9p0WvVfO4SBK5/y9JGuOty3jA6OhptxOYZgwjTTPLQ==
"@hotwired/stimulus-webpack-helpers@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd"
integrity sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ==
"@hotwired/stimulus@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608"
integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==
"@humanwhocodes/config-array@^0.13.0":
version "0.13.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
@ -1056,6 +1066,16 @@
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
"@symfony/stimulus-bridge@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@symfony/stimulus-bridge/-/stimulus-bridge-4.0.0.tgz#dbd352a373f8ff323f2895e0bb29f22b9abcc122"
integrity sha512-BfeXHAyNtE2hFJtTKFS8vLW2ElqNyzUZJJ4TMeUhrjBjYVt4/myeQxpLidy995l+TEzryE7YrnShqLviBr3pAg==
dependencies:
"@hotwired/stimulus-webpack-helpers" "^1.0.1"
"@types/webpack-env" "^1.16.4"
loader-utils "^2.0.0 || ^3.0.0"
schema-utils "^3.0.0 || ^4.0.0"
"@symfony/webpack-encore@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-5.1.0.tgz#d5664153136959e3baf3e248c9b55350c2c81a6d"
@ -1149,6 +1169,11 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==
"@types/webpack-env@^1.16.4":
version "1.18.8"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.8.tgz#71f083718c094204d7b64443701d32f1db3989e3"
integrity sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==
"@types/yargs-parser@*":
version "21.0.3"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
@ -1371,7 +1396,6 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0:
annotator@wallabag/annotator#master:
version "2.0.0-alpha.4"
uid "082069d777ed0fe5080392e85675ef3a519c7886"
resolved "https://codeload.github.com/wallabag/annotator/tar.gz/082069d777ed0fe5080392e85675ef3a519c7886"
dependencies:
backbone-extend-standalone "^0.1.2"
@ -3353,11 +3377,6 @@ jiti@^1.20.0:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
jquery.cookie@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jquery.cookie/-/jquery.cookie-1.4.1.tgz#d63dce209eab691fe63316db08ca9e47e0f9385b"
integrity sha512-c/hZOOL+8VSw/FkTVH637gS1/6YzMSCROpTZ2qBYwJ7s7sHajU7uBkSSiE5+GXWwrfCCyO+jsYjUQ7Hs2rIxAA==
jquery@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
@ -3481,6 +3500,11 @@ loader-utils@^2.0.0:
emojis-list "^3.0.0"
json5 "^2.1.2"
"loader-utils@^2.0.0 || ^3.0.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5"
integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@ -4680,7 +4704,7 @@ schema-utils@^3.0.0:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0:
"schema-utils@^3.0.0 || ^4.0.0", schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0"
integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==