From 4c748b61a5ce451af4da1eee7883765912ad7b8b Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 17 Sep 2019 21:01:44 +0200 Subject: [PATCH] Search progress --- src/query.nim | 9 +++- src/routes/search.nim | 27 +++++++--- src/routes/timeline.nim | 2 +- src/sass/general.scss | 105 +++++++++++++++++++++++++++++++++++++- src/sass/inputs.scss | 8 +++ src/views/preferences.nim | 27 +--------- src/views/renderutils.nim | 36 +++++++++++-- src/views/search.nim | 57 ++++++++++++++++++--- src/views/timeline.nim | 3 ++ 9 files changed, 227 insertions(+), 47 deletions(-) diff --git a/src/query.nim b/src/query.nim index 2baa3d9..2b64036 100644 --- a/src/query.nim +++ b/src/query.nim @@ -4,13 +4,20 @@ import types const separators = @["AND", "OR"] - validFilters = @[ + validFilters* = @[ "media", "images", "twimg", "videos", "native_video", "consumer_video", "pro_video", "links", "news", "quote", "mentions", "replies", "retweets", "nativeretweets", "verified", "safe" ] + commonFilters* = @[ + "media", "videos", "images", "links", "news", "quote" + ] + advancedFilters* = @[ + "mentions", "verified", "safe", "twimg", "native_video", + "consumer_video", "pro_video" + ] # Experimental, this might break in the future # Till then, it results in shorter urls diff --git a/src/routes/search.nim b/src/routes/search.nim index 476b3f3..5e19da7 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -11,23 +11,36 @@ export search proc createSearchRouter*(cfg: Config) = router search: get "/search": - if @"text".len == 0 or "." in @"text": - resp Http404, showError("Please enter a valid username.", cfg.title) - if @"text".len > 200: resp Http400, showError("Search input too long.", cfg.title) - if "," in @"text": - redirect("/" & @"text") + let kind = parseEnum[QueryKind](@"kind", custom) + var query = Query(kind: kind, text: @"text") - let query = Query(kind: parseEnum[QueryKind](@"kind", custom), text: @"text") + if @"retweets".len == 0: + query.excludes.add "nativeretweets" + else: + query.includes.add "nativeretweets" + + if @"replies".len == 0: + query.excludes.add "replies" + else: + query.includes.add "replies" + + for f in validFilters: + if "f-" & f in params(request): + query.filters.add f + if "e-" & f in params(request): + query.excludes.add f case query.kind of users: + if "," in @"text": + redirect("/" & @"text") let users = await getSearch[Profile](query, @"after", getAgent()) resp renderMain(renderUserSearch(users, Prefs()), Prefs(), path=getPath()) of custom: let tweets = await getSearch[Tweet](query, @"after", getAgent()) resp renderMain(renderTweetSearch(tweets, Prefs(), getPath()), Prefs(), path=getPath()) else: - resp Http404 + resp Http404, showError("Invalid search.", cfg.title) diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index 9161994..683c111 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -64,7 +64,7 @@ proc showTimeline*(name, after: string; query: Option[Query]; else: let timeline = await fetchMultiTimeline(names, after, agent, query) - html = renderTweetSearch(timeline, prefs, path) + html = renderTimelineSearch(timeline, prefs, path) return renderMain(html, prefs, title, "Multi") template respTimeline*(timeline: typed) = diff --git a/src/sass/general.scss b/src/sass/general.scss index cd12b01..982cd0e 100644 --- a/src/sass/general.scss +++ b/src/sass/general.scss @@ -10,7 +10,7 @@ @include center-panel($error_red); } -.search-panel > form { +.search-bar > form { @include center-panel($darkest-grey); button { @@ -35,3 +35,106 @@ margin-right: 8px; } } + +.search-field { + margin: 2px 5px; + + .pref-group.pref-input { + display: inline-block; + width: calc(90% - 11px); + } + + input[type="text"] { + width: calc(100% - 8px); + } + + .panel-label { + background-color: #121212; + color: #F8F8F2; + border: 1px solid #FF6C6091; + padding: 1px 6px 2px 6px; + font-size: 14px; + cursor: pointer; + margin-left: -2px; + } + + .panel-label:hover { + border: 1px solid #FF6C60; + } +} + + +#panel-toggle { + display: none; + + &:checked ~ .search-panel { + max-height: 180px; + } +} + +.pannel-label { + display: inline; +} + +.search-panel { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s; + + margin: 5px; + font-weight: initial; + text-align: left; + + > div { + line-height: 1.7em; + } + + .checkbox-container { + display: inline; + padding-right: unset; + margin-left: 23px; + } + + .checkbox { + right: unset; + left: -22px; + } + + .checkbox-container .checkbox:after { + top: -4px; + } + + .search-title { + font-weight: bold; + min-width: 60px; + display: inline-block; + } + + .exclude-extras { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s; + } + + #exclude-toggle { + display: none; + + &:checked ~ .exclude-extras { + max-height: 50px; + } + } + + .filter-extras { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s; + } + + #filter-toggle { + display: none; + + &:checked ~ .filter-extras { + max-height: 50px; + } + } +} diff --git a/src/sass/inputs.scss b/src/sass/inputs.scss index db9ad3e..cb73d2e 100644 --- a/src/sass/inputs.scss +++ b/src/sass/inputs.scss @@ -88,6 +88,10 @@ input[type="text"] { } } +.pref-group { + display: inline; +} + .preferences { button { margin: 6px 0 3px 0; @@ -103,6 +107,10 @@ input[type="text"] { max-width: 120px; } + .pref-group { + display: block; + } + .pref-input { position: relative; margin-bottom: 6px; diff --git a/src/views/preferences.nim b/src/views/preferences.nim index 567a9d0..c503cbc 100644 --- a/src/views/preferences.nim +++ b/src/views/preferences.nim @@ -1,34 +1,9 @@ -import tables, macros, strformat, strutils, xmltree +import tables, macros, strutils import karax/[karaxdsl, vdom, vstyles] import renderutils import ../types, ../prefs_impl -proc genCheckbox(pref, label: string; state: bool): VNode = - buildHtml(tdiv(class="pref-group")): - label(class="checkbox-container"): - text label - if state: input(name=pref, `type`="checkbox", checked="") - else: input(name=pref, `type`="checkbox") - span(class="checkbox") - -proc genSelect(pref, label, state: string; options: seq[string]): VNode = - buildHtml(tdiv(class="pref-group")): - label(`for`=pref): text label - select(name=pref): - for opt in options: - if opt == state: - option(value=opt, selected=""): text opt - else: - option(value=opt): text opt - -proc genInput(pref, label, state, placeholder: string): VNode = - let s = xmltree.escape(state) - let p = xmltree.escape(placeholder) - buildHtml(tdiv(class="pref-group pref-input")): - label(`for`=pref): text label - verbatim &"" - macro renderPrefs*(): untyped = result = nnkCall.newTree( ident("buildHtml"), ident("tdiv"), nnkStmtList.newTree()) diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index 23900ca..a8ebb04 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -1,4 +1,4 @@ -import strutils +import strutils, strformat, xmltree import karax/[karaxdsl, vdom] import ../types, ../utils @@ -39,9 +39,12 @@ proc linkText*(text: string; class=""): VNode = buildHtml(): a(href=url, class=class): text text -proc refererField*(path: string): VNode = +proc hiddenField*(name, value: string): VNode = buildHtml(): - verbatim "" % path + verbatim "" % [name, value] + +proc refererField*(path: string): VNode = + hiddenField("referer", path) proc iconReferer*(icon, action, path: string, title=""): VNode = buildHtml(form(`method`="get", action=action, class="icon-button")): @@ -54,3 +57,30 @@ proc buttonReferer*(action, text, path: string; class=""; `method`="post"): VNod refererField path button(`type`="submit"): text text + +proc genCheckbox*(pref, label: string; state: bool): VNode = + buildHtml(tdiv(class="pref-group")): + label(class="checkbox-container"): + text label + if state: input(name=pref, `type`="checkbox", checked="") + else: input(name=pref, `type`="checkbox") + span(class="checkbox") + +proc genInput*(pref, label, state, placeholder: string; class=""): VNode = + let s = xmltree.escape(state) + let p = xmltree.escape(placeholder) + buildHtml(tdiv(class=("pref-group pref-input " & class))): + if label.len > 0: + label(`for`=pref): text label + verbatim &"" + +proc genSelect*(pref, label, state: string; options: seq[string]): VNode = + buildHtml(tdiv(class="pref-group")): + label(`for`=pref): text label + select(name=pref): + for opt in options: + if opt == state: + option(value=opt, selected=""): text opt + else: + option(value=opt): text opt + diff --git a/src/views/search.nim b/src/views/search.nim index 4b3ef8f..3bf0983 100644 --- a/src/views/search.nim +++ b/src/views/search.nim @@ -1,4 +1,4 @@ -import strutils, strformat +import strutils, strformat, unicode import karax/[karaxdsl, vdom, vstyles] import renderutils, timeline @@ -6,13 +6,13 @@ import ".."/[types, formatters, query] proc renderSearch*(): VNode = buildHtml(tdiv(class="panel-container")): - tdiv(class="search-panel"): + tdiv(class="search-bar"): form(`method`="get", action="/search"): - verbatim "" + hiddenField("kind", "users") input(`type`="text", name="text", autofocus="", placeholder="Enter username...") button(`type`="submit"): icon "search" -proc renderTweetSearch*(timeline: Timeline; prefs: Prefs; path: string): VNode = +proc renderTimelineSearch*(timeline: Timeline; prefs: Prefs; path: string): VNode = let users = if timeline.query.isSome: get(timeline.query).fromUser else: @[] @@ -24,6 +24,45 @@ proc renderTweetSearch*(timeline: Timeline; prefs: Prefs; path: string): VNode = renderProfileTabs(timeline.query, users.join(",")) renderTimelineTweets(timeline, prefs, path) +proc renderTweetSearch*(tweets: Result[Tweet]; prefs: Prefs; path: string): VNode = + let query = if tweets.query.isSome: get(tweets.query) else: Query(kind: custom) + + buildHtml(tdiv(class="timeline-container")): + tdiv(class="timeline-header"): + form(`method`="get", action="/search", class="search-field"): + hiddenField("kind", "custom") + genInput("text", "", query.text, "Enter search...", class="pref-inline") + button(`type`="submit"): icon "search" + input(id="panel-toggle", `type`="checkbox") + label(`for`="panel-toggle", class="panel-label"): + icon "down" + tdiv(class="search-panel"): + tdiv: + span(class="search-title"): text "Include: " + genCheckbox("retweets", "Retweets", "nativeretweets" in query.includes) + genCheckbox("replies", "Replies", "replies" in query.includes) + + for f in @["filter", "exclude"]: + tdiv: + span(class="search-title"): text capitalize(f) & ":" + for i in commonFilters: + let state = + if f == "filter": i in query.filters + else: i in query.excludes + genCheckbox(&"{f[0]}-{i}", capitalize(i), state) + input(id=(&"{f}-toggle"), `type`="checkbox") + label(`for`=(&"{f}-toggle"), class=(&"{f}-label")): + icon "down" + tdiv(class=(&"{f}-extras")): + for i in advancedFilters: + let state = + if f == "filter": i in query.filters + else: i in query.excludes + genCheckbox(&"{f[0]}-{i}", i, state) + + renderSearchTabs(tweets.query) + renderTimelineTweets(tweets, prefs, path) + proc renderUserSearch*(users: Result[Profile]; prefs: Prefs): VNode = let searchText = if users.query.isSome: get(users.query).text @@ -31,11 +70,13 @@ proc renderUserSearch*(users: Result[Profile]; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline-container")): tdiv(class="timeline-header"): - form(`method`="get", action="/search"): - verbatim "" - verbatim "" % searchText + form(`method`="get", action="/search", class="search-field"): + hiddenField("kind", "users") + genInput("text", "", searchText, "Enter username...", class="pref-inline") button(`type`="submit"): icon "search" + input(id="panel-toggle", `type`="checkbox") + label(`for`="panel-toggle", class="panel-label"): + icon "down" renderSearchTabs(users.query) - renderTimelineUsers(users, prefs) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index aabdf53..12aa167 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -37,6 +37,9 @@ proc renderSearchTabs*(query: Option[Query]): VNode = var q = if query.isSome: get(query) else: Query() buildHtml(ul(class="tab")): + li(class=query.getTabClass("custom")): + q.kind = custom + a(href=genQueryUrl(q)): text "Tweets" li(class=query.getTabClass("users")): q.kind = users a(href=genQueryUrl(q)): text "Users"