diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-10-15 11:46:24 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-10-20 10:15:18 +0200 |
commit | 21e72da9ee34cec56b10c83ae0c75b4bf320dfcb (patch) | |
tree | b6c0b8208f004e1b2b37b1af54e8d4c40310d56e | |
parent | 9b3c1270bcbe4f8e30e0160da8badd43dd94871a (diff) | |
download | Shaarli-21e72da9ee34cec56b10c83ae0c75b4bf320dfcb.tar.gz Shaarli-21e72da9ee34cec56b10c83ae0c75b4bf320dfcb.tar.zst Shaarli-21e72da9ee34cec56b10c83ae0c75b4bf320dfcb.zip |
Asynchronous retrieval of bookmark's thumbnails
This feature is based general.enable_async_metadata setting and works with existing metadata.js file.
The script is compatible with any template:
- the thumbnail div bloc must have attribute
- the bookmark bloc must have attribute with the bookmark ID as value
Fixes #1564
-rw-r--r-- | application/bookmark/Bookmark.php | 18 | ||||
-rw-r--r-- | application/front/controller/admin/ManageShaareController.php | 3 | ||||
-rw-r--r-- | application/front/controller/visitor/BookmarkListController.php | 10 | ||||
-rw-r--r-- | assets/common/js/metadata.js | 106 | ||||
-rw-r--r-- | tests/bookmark/BookmarkTest.php | 44 | ||||
-rw-r--r-- | tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php | 10 | ||||
-rw-r--r-- | tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php | 55 | ||||
-rw-r--r-- | tests/front/controller/visitor/BookmarkListControllerTest.php | 57 | ||||
-rw-r--r-- | tpl/default/linklist.html | 11 | ||||
-rw-r--r-- | tpl/vintage/linklist.html | 7 |
10 files changed, 279 insertions, 42 deletions
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index ea565d1f..4810c5e6 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php | |||
@@ -378,6 +378,24 @@ class Bookmark | |||
378 | } | 378 | } |
379 | 379 | ||
380 | /** | 380 | /** |
381 | * Return true if: | ||
382 | * - the bookmark's thumbnail is not already set to false (= not found) | ||
383 | * - it's not a note | ||
384 | * - it's an HTTP(S) link | ||
385 | * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore | ||
386 | * | ||
387 | * @return bool True if the bookmark's thumbnail needs to be retrieved. | ||
388 | */ | ||
389 | public function shouldUpdateThumbnail(): bool | ||
390 | { | ||
391 | return $this->thumbnail !== false | ||
392 | && !$this->isNote() | ||
393 | && startsWith(strtolower($this->url), 'http') | ||
394 | && (null === $this->thumbnail || !is_file($this->thumbnail)) | ||
395 | ; | ||
396 | } | ||
397 | |||
398 | /** | ||
381 | * Get the Sticky. | 399 | * Get the Sticky. |
382 | * | 400 | * |
383 | * @return bool | 401 | * @return bool |
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index df2f1631..908ebae3 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php | |||
@@ -129,7 +129,8 @@ class ManageShaareController extends ShaarliAdminController | |||
129 | $bookmark->setTagsString($request->getParam('lf_tags')); | 129 | $bookmark->setTagsString($request->getParam('lf_tags')); |
130 | 130 | ||
131 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | 131 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE |
132 | && false === $bookmark->isNote() | 132 | && true !== $this->container->conf->get('general.enable_async_metadata', true) |
133 | && $bookmark->shouldUpdateThumbnail() | ||
133 | ) { | 134 | ) { |
134 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | 135 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); |
135 | } | 136 | } |
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 18368751..a8019ead 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php | |||
@@ -169,14 +169,11 @@ class BookmarkListController extends ShaarliVisitorController | |||
169 | */ | 169 | */ |
170 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool | 170 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool |
171 | { | 171 | { |
172 | // Logged in, thumbnails enabled, not a note, is HTTP | 172 | // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated |
173 | // and (never retrieved yet or no valid cache file) | ||
174 | if ($this->container->loginManager->isLoggedIn() | 173 | if ($this->container->loginManager->isLoggedIn() |
174 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
175 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | 175 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE |
176 | && false !== $bookmark->getThumbnail() | 176 | && $bookmark->shouldUpdateThumbnail() |
177 | && !$bookmark->isNote() | ||
178 | && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail())) | ||
179 | && startsWith(strtolower($bookmark->getUrl()), 'http') | ||
180 | ) { | 177 | ) { |
181 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | 178 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); |
182 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | 179 | $this->container->bookmarkService->set($bookmark, $writeDatastore); |
@@ -198,6 +195,7 @@ class BookmarkListController extends ShaarliVisitorController | |||
198 | 'page_max' => '', | 195 | 'page_max' => '', |
199 | 'search_tags' => '', | 196 | 'search_tags' => '', |
200 | 'result_count' => '', | 197 | 'result_count' => '', |
198 | 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true) | ||
201 | ]; | 199 | ]; |
202 | } | 200 | } |
203 | 201 | ||
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js index 5200b481..2b013364 100644 --- a/assets/common/js/metadata.js +++ b/assets/common/js/metadata.js | |||
@@ -1,5 +1,19 @@ | |||
1 | import he from 'he'; | 1 | import he from 'he'; |
2 | 2 | ||
3 | /** | ||
4 | * This script is used to retrieve bookmarks metadata asynchronously: | ||
5 | * - title, description and keywords while creating a new bookmark | ||
6 | * - thumbnails while visiting the bookmark list | ||
7 | * | ||
8 | * Note: it should only be included if the user is logged in | ||
9 | * and the setting general.enable_async_metadata is enabled. | ||
10 | */ | ||
11 | |||
12 | /** | ||
13 | * Removes given input loaders - used in edit link template. | ||
14 | * | ||
15 | * @param {object} loaders List of input DOM element that need to be cleared | ||
16 | */ | ||
3 | function clearLoaders(loaders) { | 17 | function clearLoaders(loaders) { |
4 | if (loaders != null && loaders.length > 0) { | 18 | if (loaders != null && loaders.length > 0) { |
5 | [...loaders].forEach((loader) => { | 19 | [...loaders].forEach((loader) => { |
@@ -8,32 +22,82 @@ function clearLoaders(loaders) { | |||
8 | } | 22 | } |
9 | } | 23 | } |
10 | 24 | ||
25 | /** | ||
26 | * AJAX request to update the thumbnail of a bookmark with the provided ID. | ||
27 | * If a thumbnail is retrieved, it updates the divElement with the image src, and displays it. | ||
28 | * | ||
29 | * @param {string} basePath Shaarli subfolder for XHR requests | ||
30 | * @param {object} divElement Main <div> DOM element containing the thumbnail placeholder | ||
31 | * @param {int} id Bookmark ID to update | ||
32 | */ | ||
33 | function updateThumb(basePath, divElement, id) { | ||
34 | const xhr = new XMLHttpRequest(); | ||
35 | xhr.open('PATCH', `${basePath}/admin/shaare/${id}/update-thumbnail`); | ||
36 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | ||
37 | xhr.responseType = 'json'; | ||
38 | xhr.onload = () => { | ||
39 | if (xhr.status !== 200) { | ||
40 | alert(`An error occurred. Return code: ${xhr.status}`); | ||
41 | } else { | ||
42 | const { response } = xhr; | ||
43 | |||
44 | if (response.thumbnail !== false) { | ||
45 | const imgElement = divElement.querySelector('img'); | ||
46 | |||
47 | imgElement.src = response.thumbnail; | ||
48 | imgElement.dataset.src = response.thumbnail; | ||
49 | imgElement.style.opacity = '1'; | ||
50 | divElement.classList.remove('hidden'); | ||
51 | } | ||
52 | } | ||
53 | }; | ||
54 | xhr.send(); | ||
55 | } | ||
56 | |||
11 | (() => { | 57 | (() => { |
58 | const basePath = document.querySelector('input[name="js_base_path"]').value; | ||
12 | const loaders = document.querySelectorAll('.loading-input'); | 59 | const loaders = document.querySelectorAll('.loading-input'); |
60 | |||
61 | /* | ||
62 | * METADATA FOR EDIT BOOKMARK PAGE | ||
63 | */ | ||
13 | const inputTitle = document.querySelector('input[name="lf_title"]'); | 64 | const inputTitle = document.querySelector('input[name="lf_title"]'); |
14 | if (inputTitle != null && inputTitle.value.length > 0) { | 65 | if (inputTitle != null) { |
15 | clearLoaders(loaders); | 66 | if (inputTitle.value.length > 0) { |
16 | return; | 67 | clearLoaders(loaders); |
17 | } | 68 | return; |
69 | } | ||
18 | 70 | ||
19 | const url = document.querySelector('input[name="lf_url"]').value; | 71 | const url = document.querySelector('input[name="lf_url"]').value; |
20 | const basePath = document.querySelector('input[name="js_base_path"]').value; | ||
21 | 72 | ||
22 | const xhr = new XMLHttpRequest(); | 73 | const xhr = new XMLHttpRequest(); |
23 | xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); | 74 | xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); |
24 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | 75 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); |
25 | xhr.onload = () => { | 76 | xhr.onload = () => { |
26 | const result = JSON.parse(xhr.response); | 77 | const result = JSON.parse(xhr.response); |
27 | Object.keys(result).forEach((key) => { | 78 | Object.keys(result).forEach((key) => { |
28 | if (result[key] !== null && result[key].length) { | 79 | if (result[key] !== null && result[key].length) { |
29 | const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); | 80 | const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); |
30 | if (element != null && element.value.length === 0) { | 81 | if (element != null && element.value.length === 0) { |
31 | element.value = he.decode(result[key]); | 82 | element.value = he.decode(result[key]); |
83 | } | ||
32 | } | 84 | } |
33 | } | 85 | }); |
34 | }); | 86 | clearLoaders(loaders); |
35 | clearLoaders(loaders); | 87 | }; |
36 | }; | ||
37 | 88 | ||
38 | xhr.send(); | 89 | xhr.send(); |
90 | } | ||
91 | |||
92 | /* | ||
93 | * METADATA FOR THUMBNAIL RETRIEVAL | ||
94 | */ | ||
95 | const thumbsToLoad = document.querySelectorAll('div[data-async-thumbnail]'); | ||
96 | if (thumbsToLoad != null) { | ||
97 | [...thumbsToLoad].forEach((divElement) => { | ||
98 | const { id } = divElement.closest('[data-id]').dataset; | ||
99 | |||
100 | updateThumb(basePath, divElement, id); | ||
101 | }); | ||
102 | } | ||
39 | })(); | 103 | })(); |
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php index 4c7ae4c0..4c1ae25d 100644 --- a/tests/bookmark/BookmarkTest.php +++ b/tests/bookmark/BookmarkTest.php | |||
@@ -347,4 +347,48 @@ class BookmarkTest extends TestCase | |||
347 | $bookmark->deleteTag('nope'); | 347 | $bookmark->deleteTag('nope'); |
348 | $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); | 348 | $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); |
349 | } | 349 | } |
350 | |||
351 | /** | ||
352 | * Test shouldUpdateThumbnail() with bookmarks needing an update. | ||
353 | */ | ||
354 | public function testShouldUpdateThumbnail(): void | ||
355 | { | ||
356 | $bookmark = (new Bookmark())->setUrl('http://domain.tld/with-image'); | ||
357 | |||
358 | static::assertTrue($bookmark->shouldUpdateThumbnail()); | ||
359 | |||
360 | $bookmark = (new Bookmark()) | ||
361 | ->setUrl('http://domain.tld/with-image') | ||
362 | ->setThumbnail('unknown file') | ||
363 | ; | ||
364 | |||
365 | static::assertTrue($bookmark->shouldUpdateThumbnail()); | ||
366 | } | ||
367 | |||
368 | /** | ||
369 | * Test shouldUpdateThumbnail() with bookmarks that should not update. | ||
370 | */ | ||
371 | public function testShouldNotUpdateThumbnail(): void | ||
372 | { | ||
373 | $bookmark = (new Bookmark()); | ||
374 | |||
375 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
376 | |||
377 | $bookmark = (new Bookmark()) | ||
378 | ->setUrl('ftp://domain.tld/other-protocol', ['ftp']) | ||
379 | ; | ||
380 | |||
381 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
382 | |||
383 | $bookmark = (new Bookmark()) | ||
384 | ->setUrl('http://domain.tld/with-image') | ||
385 | ->setThumbnail(__FILE__) | ||
386 | ; | ||
387 | |||
388 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
389 | |||
390 | $bookmark = (new Bookmark())->setUrl('/shaare/abcdef'); | ||
391 | |||
392 | static::assertFalse($bookmark->shouldUpdateThumbnail()); | ||
393 | } | ||
350 | } | 394 | } |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php index 4fd88480..eafa54eb 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php +++ b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php | |||
@@ -144,12 +144,14 @@ class DisplayCreateFormTest extends TestCase | |||
144 | 144 | ||
145 | // Make sure that PluginManager hook is triggered | 145 | // Make sure that PluginManager hook is triggered |
146 | $this->container->pluginManager | 146 | $this->container->pluginManager |
147 | ->expects(static::at(0)) | 147 | ->expects(static::atLeastOnce()) |
148 | ->method('executeHooks') | 148 | ->method('executeHooks') |
149 | ->withConsecutive(['render_editlink'], ['render_includes']) | ||
149 | ->willReturnCallback(function (string $hook, array $data): array { | 150 | ->willReturnCallback(function (string $hook, array $data): array { |
150 | static::assertSame('render_editlink', $hook); | 151 | if ('render_editlink' === $hook) { |
151 | static::assertSame('', $data['link']['title']); | 152 | static::assertSame('', $data['link']['title']); |
152 | static::assertSame('', $data['link']['description']); | 153 | static::assertSame('', $data['link']['description']); |
154 | } | ||
153 | 155 | ||
154 | return $data; | 156 | return $data; |
155 | }) | 157 | }) |
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php index 37542c26..1adeef5a 100644 --- a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php +++ b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php | |||
@@ -209,7 +209,7 @@ class SaveBookmarkTest extends TestCase | |||
209 | /** | 209 | /** |
210 | * Test save a bookmark - try to retrieve the thumbnail | 210 | * Test save a bookmark - try to retrieve the thumbnail |
211 | */ | 211 | */ |
212 | public function testSaveBookmarkWithThumbnail(): void | 212 | public function testSaveBookmarkWithThumbnailSync(): void |
213 | { | 213 | { |
214 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; | 214 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; |
215 | 215 | ||
@@ -224,7 +224,13 @@ class SaveBookmarkTest extends TestCase | |||
224 | 224 | ||
225 | $this->container->conf = $this->createMock(ConfigManager::class); | 225 | $this->container->conf = $this->createMock(ConfigManager::class); |
226 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | 226 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { |
227 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | 227 | if ($key === 'thumbnails.mode') { |
228 | return Thumbnailer::MODE_ALL; | ||
229 | } elseif ($key === 'general.enable_async_metadata') { | ||
230 | return false; | ||
231 | } | ||
232 | |||
233 | return $default; | ||
228 | }); | 234 | }); |
229 | 235 | ||
230 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | 236 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); |
@@ -275,6 +281,51 @@ class SaveBookmarkTest extends TestCase | |||
275 | } | 281 | } |
276 | 282 | ||
277 | /** | 283 | /** |
284 | * Test save a bookmark - do not attempt to retrieve thumbnails if async mode is enabled. | ||
285 | */ | ||
286 | public function testSaveBookmarkWithThumbnailAsync(): void | ||
287 | { | ||
288 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; | ||
289 | |||
290 | $request = $this->createMock(Request::class); | ||
291 | $request | ||
292 | ->method('getParam') | ||
293 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
294 | return $parameters[$key] ?? null; | ||
295 | }) | ||
296 | ; | ||
297 | $response = new Response(); | ||
298 | |||
299 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
300 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
301 | if ($key === 'thumbnails.mode') { | ||
302 | return Thumbnailer::MODE_ALL; | ||
303 | } elseif ($key === 'general.enable_async_metadata') { | ||
304 | return true; | ||
305 | } | ||
306 | |||
307 | return $default; | ||
308 | }); | ||
309 | |||
310 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | ||
311 | $this->container->thumbnailer->expects(static::never())->method('get'); | ||
312 | |||
313 | $this->container->bookmarkService | ||
314 | ->expects(static::once()) | ||
315 | ->method('addOrSet') | ||
316 | ->willReturnCallback(function (Bookmark $bookmark): Bookmark { | ||
317 | static::assertNull($bookmark->getThumbnail()); | ||
318 | |||
319 | return $bookmark; | ||
320 | }) | ||
321 | ; | ||
322 | |||
323 | $result = $this->controller->save($request, $response); | ||
324 | |||
325 | static::assertSame(302, $result->getStatusCode()); | ||
326 | } | ||
327 | |||
328 | /** | ||
278 | * Change the password with a wrong existing password | 329 | * Change the password with a wrong existing password |
279 | */ | 330 | */ |
280 | public function testSaveBookmarkFromBookmarklet(): void | 331 | public function testSaveBookmarkFromBookmarklet(): void |
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php index 0c95df97..5ca92507 100644 --- a/tests/front/controller/visitor/BookmarkListControllerTest.php +++ b/tests/front/controller/visitor/BookmarkListControllerTest.php | |||
@@ -307,7 +307,13 @@ class BookmarkListControllerTest extends TestCase | |||
307 | $this->container->conf | 307 | $this->container->conf |
308 | ->method('get') | 308 | ->method('get') |
309 | ->willReturnCallback(function (string $key, $default) { | 309 | ->willReturnCallback(function (string $key, $default) { |
310 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | 310 | if ($key === 'thumbnails.mode') { |
311 | return Thumbnailer::MODE_ALL; | ||
312 | } elseif ($key === 'general.enable_async_metadata') { | ||
313 | return false; | ||
314 | } | ||
315 | |||
316 | return $default; | ||
311 | }) | 317 | }) |
312 | ; | 318 | ; |
313 | 319 | ||
@@ -357,7 +363,13 @@ class BookmarkListControllerTest extends TestCase | |||
357 | $this->container->conf | 363 | $this->container->conf |
358 | ->method('get') | 364 | ->method('get') |
359 | ->willReturnCallback(function (string $key, $default) { | 365 | ->willReturnCallback(function (string $key, $default) { |
360 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | 366 | if ($key === 'thumbnails.mode') { |
367 | return Thumbnailer::MODE_ALL; | ||
368 | } elseif ($key === 'general.enable_async_metadata') { | ||
369 | return false; | ||
370 | } | ||
371 | |||
372 | return $default; | ||
361 | }) | 373 | }) |
362 | ; | 374 | ; |
363 | 375 | ||
@@ -379,6 +391,47 @@ class BookmarkListControllerTest extends TestCase | |||
379 | } | 391 | } |
380 | 392 | ||
381 | /** | 393 | /** |
394 | * Test getting a permalink with thumbnail update with async setting: no update should run. | ||
395 | */ | ||
396 | public function testThumbnailUpdateFromPermalinkAsync(): void | ||
397 | { | ||
398 | $request = $this->createMock(Request::class); | ||
399 | $response = new Response(); | ||
400 | |||
401 | $this->container->loginManager = $this->createMock(LoginManager::class); | ||
402 | $this->container->loginManager->method('isLoggedIn')->willReturn(true); | ||
403 | |||
404 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
405 | $this->container->conf | ||
406 | ->method('get') | ||
407 | ->willReturnCallback(function (string $key, $default) { | ||
408 | if ($key === 'thumbnails.mode') { | ||
409 | return Thumbnailer::MODE_ALL; | ||
410 | } elseif ($key === 'general.enable_async_metadata') { | ||
411 | return true; | ||
412 | } | ||
413 | |||
414 | return $default; | ||
415 | }) | ||
416 | ; | ||
417 | |||
418 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | ||
419 | $this->container->thumbnailer->expects(static::never())->method('get'); | ||
420 | |||
421 | $this->container->bookmarkService | ||
422 | ->expects(static::once()) | ||
423 | ->method('findByHash') | ||
424 | ->willReturn((new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1')) | ||
425 | ; | ||
426 | $this->container->bookmarkService->expects(static::never())->method('set'); | ||
427 | $this->container->bookmarkService->expects(static::never())->method('save'); | ||
428 | |||
429 | $result = $this->controller->permalink($request, $response, ['hash' => 'abc']); | ||
430 | |||
431 | static::assertSame(200, $result->getStatusCode()); | ||
432 | } | ||
433 | |||
434 | /** | ||
382 | * Trigger legacy controller in link list controller: permalink | 435 | * Trigger legacy controller in link list controller: permalink |
383 | */ | 436 | */ |
384 | public function testLegacyControllerPermalink(): void | 437 | public function testLegacyControllerPermalink(): void |
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index beab0eac..48cd9aad 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html | |||
@@ -135,8 +135,12 @@ | |||
135 | 135 | ||
136 | <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> | 136 | <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> |
137 | <div class="linklist-item-title"> | 137 | <div class="linklist-item-title"> |
138 | {if="$thumbnails_enabled && !empty($value.thumbnail)"} | 138 | {if="$thumbnails_enabled && $value.thumbnail !== false"} |
139 | <div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"> | 139 | <div |
140 | class="linklist-item-thumbnail {if="$value.thumbnail === null"}hidden{/if}" | ||
141 | style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;" | ||
142 | {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if} | ||
143 | > | ||
140 | <div class="thumbnail"> | 144 | <div class="thumbnail"> |
141 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} | 145 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} |
142 | <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> | 146 | <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> |
@@ -158,7 +162,7 @@ | |||
158 | </div> | 162 | </div> |
159 | 163 | ||
160 | <h2> | 164 | <h2> |
161 | <a href="{$value.real_url}"> | 165 | <a href="{$value.real_url}" class="linklist-real-url"> |
162 | {if="strpos($value.url, $value.shorturl) === false"} | 166 | {if="strpos($value.url, $value.shorturl) === false"} |
163 | <i class="fa fa-external-link" aria-hidden="true"></i> | 167 | <i class="fa fa-external-link" aria-hidden="true"></i> |
164 | {else} | 168 | {else} |
@@ -308,5 +312,6 @@ | |||
308 | 312 | ||
309 | {include="page.footer"} | 313 | {include="page.footer"} |
310 | <script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script> | 314 | <script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script> |
315 | {if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} | ||
311 | </body> | 316 | </body> |
312 | </html> | 317 | </html> |
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index 00896eb5..90f5cf8f 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html | |||
@@ -77,10 +77,10 @@ | |||
77 | {/if} | 77 | {/if} |
78 | <ul> | 78 | <ul> |
79 | {loop="$links"} | 79 | {loop="$links"} |
80 | <li{if="$value.class"} class="{$value.class}"{/if}> | 80 | <li{if="$value.class"} class="{$value.class}"{/if} data-id="{$value.id}"> |
81 | <a id="{$value.shorturl}"></a> | 81 | <a id="{$value.shorturl}"></a> |
82 | {if="$thumbnails_enabled && !empty($value.thumbnail)"} | 82 | {if="$thumbnails_enabled && $value.thumbnail !== false"} |
83 | <div class="thumbnail"> | 83 | <div class="thumbnail" {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}> |
84 | <a href="{$value.real_url}"> | 84 | <a href="{$value.real_url}"> |
85 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} | 85 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} |
86 | <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" | 86 | <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" |
@@ -153,6 +153,7 @@ | |||
153 | 153 | ||
154 | {include="page.footer"} | 154 | {include="page.footer"} |
155 | <script src="{$asset_path}/js/thumbnails.min.js#"></script> | 155 | <script src="{$asset_path}/js/thumbnails.min.js#"></script> |
156 | {if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} | ||
156 | 157 | ||
157 | </body> | 158 | </body> |
158 | </html> | 159 | </html> |