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"