aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/LinkDB.php7
-rw-r--r--application/LinkFilter.php41
-rw-r--r--application/PageBuilder.php1
-rw-r--r--index.php16
-rw-r--r--tests/LinkFilterTest.php3
-rw-r--r--tpl/default/css/shaarli.css45
-rw-r--r--tpl/default/linklist.html50
-rw-r--r--tpl/default/linklist.paging.html11
-rw-r--r--tpl/default/page.header.html39
9 files changed, 111 insertions, 102 deletions
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 8ca0fab3..9e3efd6b 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -417,21 +417,22 @@ You use the community supported version of the original Shaarli project, by Seba
417 * - searchterm: term search 417 * - searchterm: term search
418 * @param bool $casesensitive Optional: Perform case sensitive filter 418 * @param bool $casesensitive Optional: Perform case sensitive filter
419 * @param string $visibility return only all/private/public links 419 * @param string $visibility return only all/private/public links
420 * @param string $untaggedonly return only untagged links
420 * 421 *
421 * @return array filtered links, all links if no suitable filter was provided. 422 * @return array filtered links, all links if no suitable filter was provided.
422 */ 423 */
423 public function filterSearch($filterRequest = array(), $casesensitive = false, $visibility = 'all') 424 public function filterSearch($filterRequest = array(), $casesensitive = false, $visibility = 'all', $untaggedonly = false)
424 { 425 {
425 // Filter link database according to parameters. 426 // Filter link database according to parameters.
426 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; 427 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
427 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; 428 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
428 429
429 // Search tags + fullsearch - blank string parameter will return all links. 430 // Search tags + fullsearch - blank string parameter will return all links.
430 $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; 431 $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext"
431 $request = [$searchtags, $searchterm]; 432 $request = [$searchtags, $searchterm];
432 433
433 $linkFilter = new LinkFilter($this); 434 $linkFilter = new LinkFilter($this);
434 return $linkFilter->filter($type, $request, $casesensitive, $visibility); 435 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
435 } 436 }
436 437
437 /** 438 /**
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 0e887d38..95519528 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -52,10 +52,11 @@ class LinkFilter
52 * @param mixed $request Filter content. 52 * @param mixed $request Filter content.
53 * @param bool $casesensitive Optional: Perform case sensitive filter if true. 53 * @param bool $casesensitive Optional: Perform case sensitive filter if true.
54 * @param string $visibility Optional: return only all/private/public links 54 * @param string $visibility Optional: return only all/private/public links
55 * @param string $untaggedonly Optional: return only untagged links. Applies only if $type includes FILTER_TAG
55 * 56 *
56 * @return array filtered link list. 57 * @return array filtered link list.
57 */ 58 */
58 public function filter($type, $request, $casesensitive = false, $visibility = 'all') 59 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
59 { 60 {
60 if (! in_array($visibility, ['all', 'public', 'private'])) { 61 if (! in_array($visibility, ['all', 'public', 'private'])) {
61 $visibility = 'all'; 62 $visibility = 'all';
@@ -64,23 +65,34 @@ class LinkFilter
64 switch($type) { 65 switch($type) {
65 case self::$FILTER_HASH: 66 case self::$FILTER_HASH:
66 return $this->filterSmallHash($request); 67 return $this->filterSmallHash($request);
67 case self::$FILTER_TAG | self::$FILTER_TEXT: 68 case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
68 if (!empty($request)) { 69 $noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
69 $filtered = $this->links; 70 if ($noRequest) {
70 if (isset($request[0])) { 71 if ($untaggedonly) {
71 $filtered = $this->filterTags($request[0], $casesensitive, $visibility); 72 return $this->filterUntagged($visibility);
72 }
73 if (isset($request[1])) {
74 $lf = new LinkFilter($filtered);
75 $filtered = $lf->filterFulltext($request[1], $visibility);
76 } 73 }
77 return $filtered; 74 return $this->noFilter($visibility);
78 } 75 }
79 return $this->noFilter($visibility); 76 if ($untaggedonly) {
77 $filtered = $this->filterUntagged($visibility);
78 } else {
79 $filtered = $this->links;
80 }
81 if (!empty($request[0])) {
82 $filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
83 }
84 if (!empty($request[1])) {
85 $filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility);
86 }
87 return $filtered;
80 case self::$FILTER_TEXT: 88 case self::$FILTER_TEXT:
81 return $this->filterFulltext($request, $visibility); 89 return $this->filterFulltext($request, $visibility);
82 case self::$FILTER_TAG: 90 case self::$FILTER_TAG:
83 return $this->filterTags($request, $casesensitive, $visibility); 91 if ($untaggedonly) {
92 return $this->filterUntagged($visibility);
93 } else {
94 return $this->filterTags($request, $casesensitive, $visibility);
95 }
84 case self::$FILTER_DAY: 96 case self::$FILTER_DAY:
85 return $this->filterDay($request); 97 return $this->filterDay($request);
86 default: 98 default:
@@ -253,9 +265,6 @@ class LinkFilter
253 { 265 {
254 // Implode if array for clean up. 266 // Implode if array for clean up.
255 $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags; 267 $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags;
256 if ($tags === false) {
257 return $this->filterUntagged($visibility);
258 }
259 if (empty($tags)) { 268 if (empty($tags)) {
260 return $this->noFilter($visibility); 269 return $this->noFilter($visibility);
261 } 270 }
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index c86621a2..7a42400d 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -78,6 +78,7 @@ class PageBuilder
78 $this->tpl->assign('version', shaarli_version); 78 $this->tpl->assign('version', shaarli_version);
79 $this->tpl->assign('scripturl', index_url($_SERVER)); 79 $this->tpl->assign('scripturl', index_url($_SERVER));
80 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? 80 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
81 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
81 $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli')); 82 $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli'));
82 if ($this->conf->exists('general.header_link')) { 83 if ($this->conf->exists('general.header_link')) {
83 $this->tpl->assign('titleLink', $this->conf->get('general.header_link')); 84 $this->tpl->assign('titleLink', $this->conf->get('general.header_link'));
diff --git a/index.php b/index.php
index 85486eb5..5c292b04 100644
--- a/index.php
+++ b/index.php
@@ -287,6 +287,7 @@ function logout() {
287 unset($_SESSION['ip']); 287 unset($_SESSION['ip']);
288 unset($_SESSION['username']); 288 unset($_SESSION['username']);
289 unset($_SESSION['privateonly']); 289 unset($_SESSION['privateonly']);
290 unset($_SESSION['untaggedonly']);
290 } 291 }
291 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); 292 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
292} 293}
@@ -1017,6 +1018,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1017 exit; 1018 exit;
1018 } 1019 }
1019 1020
1021 // -------- User wants to see only untagged links (toggle)
1022 if (isset($_GET['untaggedonly'])) {
1023 $_SESSION['untaggedonly'] = !$_SESSION['untaggedonly'];
1024
1025 if (! empty($_SERVER['HTTP_REFERER'])) {
1026 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
1027 } else {
1028 $location = '?';
1029 }
1030 header('Location: '. $location);
1031 exit;
1032 }
1033
1020 // -------- Handle other actions allowed for non-logged in users: 1034 // -------- Handle other actions allowed for non-logged in users:
1021 if (!isLoggedIn()) 1035 if (!isLoggedIn())
1022 { 1036 {
@@ -1651,7 +1665,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1651 'searchtags' => $searchtags, 1665 'searchtags' => $searchtags,
1652 'searchterm' => $searchterm, 1666 'searchterm' => $searchterm,
1653 ]; 1667 ];
1654 $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility); 1668 $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly']));
1655 } 1669 }
1656 1670
1657 // ---- Handle paging. 1671 // ---- Handle paging.
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php
index 74162358..d796d3a3 100644
--- a/tests/LinkFilterTest.php
+++ b/tests/LinkFilterTest.php
@@ -63,10 +63,9 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
63 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) 63 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, ''))
64 ); 64 );
65 65
66 // Untagged only
67 $this->assertEquals( 66 $this->assertEquals(
68 self::$refDB->countUntaggedLinks(), 67 self::$refDB->countUntaggedLinks(),
69 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, false)) 68 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, /*$request=*/'', /*$casesensitive=*/false, /*$visibility=*/'all', /*$untaggedonly=*/true))
70 ); 69 );
71 70
72 $this->assertEquals( 71 $this->assertEquals(
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css
index 39bbd0a3..e1868c59 100644
--- a/tpl/default/css/shaarli.css
+++ b/tpl/default/css/shaarli.css
@@ -226,6 +226,12 @@ body, .pure-g [class*="pure-u"] {
226 border-radius: 2px; 226 border-radius: 2px;
227 color: #252525; 227 color: #252525;
228} 228}
229@media screen and (max-width: 64em) {
230 .searchform {
231 max-width: 260px;
232 margin: 0 auto;
233 }
234}
229 235
230/* because chrome */ 236/* because chrome */
231#search input[type="text"]::-webkit-input-placeholder, 237#search input[type="text"]::-webkit-input-placeholder,
@@ -236,43 +242,37 @@ body, .pure-g [class*="pure-u"] {
236#search button, 242#search button,
237#search-tagcloud button, 243#search-tagcloud button,
238#search-linklist button { 244#search-linklist button {
239 background: transparent; 245 padding: 4px 8px 6px 8px;
246 background-color: #1B926C;
247 color: #f5f5f5;
240 border: none; 248 border: none;
249 border-radius: 2px;
241} 250}
242 251
243#search button { 252#search-tagcloud button {
244 color: #f5f5f5; 253 width: 90%;
245} 254}
246 255
247#search-linklist button { 256@media screen and (max-width: 64em) {
248 color: #252525; 257 #search-linklist button {
258 width: 100%;
259 }
260 #search-linklist .awesomplete {
261 margin: 5px 0;
262 }
249} 263}
250 264
251#search button:hover, 265#search button:hover,
252#search-linklist button:hover { 266#search-linklist button:hover,
253 color: #fff;
254}
255#search-tagcloud button:hover { 267#search-tagcloud button:hover {
256 color: #d0d0d0; 268 color: #d0d0d0;
257} 269}
258 270
271#search,
259#search-linklist { 272#search-linklist {
260 padding: 5px 0; 273 padding: 6px 0;
261} 274}
262 275
263@media screen and (min-width: 64em) {
264 #search .searchform,
265 #search-linklist .searchform {
266 margin-right: 25px;
267 text-align: right;
268 }
269
270 #search .tagfilter,
271 #search-linklist .tagfilter {
272 margin-left: 25px;
273 text-align: left;
274 }
275}
276@media screen and (max-width: 64em) { 276@media screen and (max-width: 64em) {
277 #search, #search * { 277 #search, #search * {
278 visibility: hidden; 278 visibility: hidden;
@@ -321,7 +321,6 @@ body, .pure-g [class*="pure-u"] {
321} 321}
322 322
323.subheader-form input[type="text"], .subheader-form input[type="password"], .subheader-form .remember-me { 323.subheader-form input[type="text"], .subheader-form input[type="password"], .subheader-form .remember-me {
324 margin: 0 0 5px 0;
325 padding: 5px 5px 3px 15px; 324 padding: 5px 5px 3px 15px;
326 height: 20px; 325 height: 20px;
327 width: 20%; 326 width: 20%;
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 2568a5d6..685821e3 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -19,30 +19,21 @@
19 19
20<div id="search-linklist"> 20<div id="search-linklist">
21 21
22 <div class="pure-g"> 22 <form method="GET" class="pure-form searchform" name="searchform">
23 <div class="pure-u-1 pure-u-lg-1-2"> 23 <input type="text" tabindex="1" name="searchterm" class="searchterm" placeholder="{'Search text'|t}"
24 <form method="GET" class="searchform" name="searchform"> 24 {if="!empty($search_term)"}
25 <input type="text" tabindex="1" name="searchterm" placeholder="{'Search text'|t}" 25 value="{$search_term}"
26 {if="!empty($search_term)"} 26 {/if}
27 value="{$search_term}" 27 >
28 {/if} 28 <input type="text" tabindex="2" name="searchtags" class="searchtags" placeholder="{'Filter by tag'|t}"
29 > 29 {if="!empty($search_tags)"}
30 <button type="submit" class="search-button"><i class="fa fa-search"></i></button> 30 value="{$search_tags}"
31 </form> 31 {/if}
32 </div> 32 autocomplete="off" data-multiple data-autofirst data-minChars="1"
33 <div class="pure-u-1 pure-u-lg-1-2"> 33 data-list="{loop="$tags"}{$key}, {/loop}"
34 <form method="GET" class="tagfilter" name="tagfilter"> 34 >
35 <input type="text" tabindex="2" name="searchtags" placeholder="{'Filter by tag'|t}" 35 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
36 {if="!empty($search_tags)"} 36 </form>
37 value="{$search_tags}"
38 {/if}
39 autocomplete="off" data-multiple data-autofirst data-minChars="1"
40 data-list="{loop="$tags"}{$key}, {/loop}"
41 >
42 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
43 </form>
44 </div>
45 </div>
46</div> 37</div>
47 38
48{loop="$plugins_header.fields_toolbar"} 39{loop="$plugins_header.fields_toolbar"}
@@ -91,7 +82,7 @@
91 <div id="searchcriteria">{'Nothing found.'|t}</div> 82 <div id="searchcriteria">{'Nothing found.'|t}</div>
92 </div> 83 </div>
93 </div> 84 </div>
94 {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility)"} 85 {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility) or $untaggedonly"}
95 <div class="pure-g pure-alert pure-alert-success search-result"> 86 <div class="pure-g pure-alert pure-alert-success search-result">
96 <div class="pure-u-2-24"></div> 87 <div class="pure-u-2-24"></div>
97 <div class="pure-u-20-24"> 88 <div class="pure-u-20-24">
@@ -107,10 +98,6 @@
107 <a href="?removetag={function="urlencode($value)"}">{$value}<span class="remove"><i class="fa fa-times"></i></span></a> 98 <a href="?removetag={function="urlencode($value)"}">{$value}<span class="remove"><i class="fa fa-times"></i></span></a>
108 </span> 99 </span>
109 {/loop} 100 {/loop}
110 {elseif="$search_tags === false"}
111 <span class="label label-tag" title="{'Remove tag'|t}">
112 <a href="?">{'untagged'|t}<span class="remove"><i class="fa fa-times"></i></span></a>
113 </span>
114 {/if} 101 {/if}
115 {if="!empty($visibility)"} 102 {if="!empty($visibility)"}
116 {'with status'|t} 103 {'with status'|t}
@@ -118,6 +105,11 @@
118 {$visibility|t} 105 {$visibility|t}
119 </span> 106 </span>
120 {/if} 107 {/if}
108 {if="$untaggedonly"}
109 <span class="label label-private">
110 {'without any tag'|t}
111 </span>
112 {/if}
121 </div> 113 </div>
122 </div> 114 </div>
123 {/if} 115 {/if}
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index d8c1e76e..41e9fa34 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -6,10 +6,13 @@
6 {'Filters'|t} 6 {'Filters'|t}
7 </span> 7 </span>
8 {if="isLoggedIn()"} 8 {if="isLoggedIn()"}
9 <a href="?privateonly" title="{'Filter private links'|t}" 9 <a href="?privateonly" title="{'Filter private links'|t}"
10 class={if="$privateonly"}"filter-on"{else}"filter-off"{/if} 10 class={if="$privateonly"}"filter-on"{else}"filter-off"{/if}
11 ><i class="fa fa-key"></i></a> 11 ><i class="fa fa-key"></i></a>
12 {/if} 12 {/if}
13 <a href="?untaggedonly" title="{'Filter untagged links'|t}"
14 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
15 ><i class="fa fa-tag"></i></a>
13 <a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all"> 16 <a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all">
14 <i class="fa fa-chevron-up"></i> 17 <i class="fa fa-chevron-up"></i>
15 </a> 18 </a>
@@ -55,4 +58,4 @@
55 </a> 58 </a>
56 </div> 59 </div>
57 </div> 60 </div>
58</div> \ No newline at end of file 61</div>
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 6c71a718..2411703c 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -97,30 +97,21 @@
97 97
98<div id="content"> 98<div id="content">
99 <div id="search" class="subheader-form"> 99 <div id="search" class="subheader-form">
100 <div class="pure-g"> 100 <form method="GET" class="pure-form searchform" name="searchform">
101 <div class="pure-u-1 pure-u-lg-1-2"> 101 <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="{'Search text'|t}"
102 <form method="GET" class="searchform" name="searchform"> 102 {if="!empty($search_term)"}
103 <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="{'Search text'|t}" 103 value="{$search_term}"
104 {if="!empty($search_term)"} 104 {/if}
105 value="{$search_term}" 105 >
106 {/if} 106 <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="{'Filter by tag'|t}"
107 > 107 {if="!empty($search_tags)"}
108 <button type="submit" class="search-button"><i class="fa fa-search"></i></button> 108 value="{$search_tags}"
109 </form> 109 {/if}
110 </div> 110 autocomplete="off" data-multiple data-autofirst data-minChars="1"
111 <div class="pure-u-1 pure-u-lg-1-2"> 111 data-list="{loop="$tags"}{$key}, {/loop}"
112 <form method="GET" class="tagfilter" name="tagfilter"> 112 >
113 <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="{'Filter by tag'|t}" 113 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
114 {if="!empty($search_tags)"} 114 </form>
115 value="{$search_tags}"
116 {/if}
117 autocomplete="off" data-multiple data-autofirst data-minChars="1"
118 data-list="{loop="$tags"}{$key}, {/loop}"
119 >
120 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
121 </form>
122 </div>
123 </div>
124 </div> 115 </div>
125 <div id="actions" class="subheader-form"> 116 <div id="actions" class="subheader-form">
126 <div class="pure-g"> 117 <div class="pure-g">