Quellcode durchsuchen

Redesign and fix search, add custom timeline tab

Zed vor 7 Jahren
Ursprung
Commit
7d7eb085ca

+ 22 - 26
src/query.nim

@@ -1,4 +1,4 @@
-import strutils, strformat, sequtils
+import strutils, strformat, sequtils, tables
 
 import types
 
@@ -11,13 +11,6 @@ const
     "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
@@ -25,18 +18,22 @@ const
   posPrefix = "thGAVUV0VFVBa"
   posSuffix = "EjUAFQAlAFUAFQAA"
 
-proc initQuery*(filters, includes, excludes, separator, text: string; name=""): Query =
-  var sep = separator.strip().toUpper()
-  Query(
-    kind: custom,
-    text: text,
-    filters: filters.split(",").filterIt(it in validFilters),
-    includes: includes.split(",").filterIt(it in validFilters),
-    excludes: excludes.split(",").filterIt(it in validFilters),
+template `@`(param: string): untyped =
+  if param in pms: pms[param]
+  else: ""
+
+proc initQuery*(pms: Table[string, string]; name=""): Query =
+  result = Query(
+    kind: parseEnum[QueryKind](@"kind", custom),
+    text: @"text",
     fromUser: @[name],
-    sep: if sep in separators: sep else: ""
+    filters: validFilters.filterIt("f-" & it in pms),
+    excludes: validFilters.filterIt("e-" & it in pms),
   )
 
+  if @"e-nativeretweets".len == 0:
+    result.includes.add "nativeretweets"
+
 proc getMediaQuery*(name: string): Query =
   Query(
     kind: media,
@@ -88,16 +85,15 @@ proc genQueryUrl*(query: Query): string =
   result &= &"/search?"
 
   var params = @[&"kind={query.kind}"]
-  if query.filters.len > 0:
-    params &= "filter=" & query.filters.join(",")
-  if query.includes.len > 0:
-    params &= "include=" & query.includes.join(",")
-  if query.excludes.len > 0:
-    params &= "not=" & query.excludes.join(",")
-  if query.sep.len > 0:
-    params &= "sep=" & query.sep
   if query.text.len > 0:
-    params &= "text=" & query.text
+    params.add "text=" & query.text
+  for f in query.filters:
+    params.add "f-" & f & "=on"
+  for e in query.excludes:
+    params.add "e-" & e & "=on"
+  for i in query.excludes:
+    params.add "i-" & i & "=on"
+
   if params.len > 0:
     result &= params.join("&")
 

+ 2 - 19
src/routes/search.nim

@@ -1,4 +1,4 @@
-import strutils, uri
+import strutils, sequtils, uri
 
 import jester
 
@@ -14,24 +14,7 @@ proc createSearchRouter*(cfg: Config) =
       if @"text".len > 200:
         resp Http400, showError("Search input too long.", cfg.title)
 
-      let kind = parseEnum[QueryKind](@"kind", custom)
-      var query = Query(kind: kind, 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
+      let query = initQuery(params(request))
 
       case query.kind
       of users:

+ 4 - 4
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 = renderTimelineSearch(timeline, prefs, path)
+      html = renderTweetSearch(timeline, prefs, path)
     return renderMain(html, prefs, title, "Multi")
 
 template respTimeline*(timeline: typed) =
@@ -84,9 +84,9 @@ proc createTimelineRouter*(cfg: Config) =
 
     get "/@name/search":
       cond '.' notin @"name"
-      let query = some initQuery(@"filter", @"include", @"not", @"sep", @"text", @"name")
-      respTimeline(await showTimeline(@"name", @"after", query,
-                                      cookiePrefs(), getPath(), cfg.title, ""))
+      let query = some initQuery(params(request), name=(@"name"))
+      respTimeline(await showTimeline(@"name", @"after", query, cookiePrefs(),
+                                      getPath(), cfg.title, ""))
 
     get "/@name/replies":
       cond '.' notin @"name"

+ 0 - 103
src/sass/general.scss

@@ -35,106 +35,3 @@
         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;
-        }
-    }
-}

+ 26 - 0
src/sass/include/_mixins.css

@@ -58,3 +58,29 @@
         border-color: $accent_light;
     }
 }
+
+@mixin search-resize($width, $rows, $height) {
+    @media(max-width: $width) {
+        .search-toggles {
+            grid-template-columns: repeat($rows, auto);
+        }
+
+        #search-panel-toggle:checked ~ .search-panel {
+            max-height: $height !important;
+        }
+    }
+}
+
+@mixin create-toggle($elem, $height) {
+    ##{$elem}-toggle {
+        display: none;
+
+        &:checked ~ .#{$elem} {
+            max-height: $height;
+        }
+
+        &:checked ~ label .icon-down:before {
+            transform: rotate(180deg) translateY(-1px);
+        }
+    }
+}

+ 1 - 0
src/sass/index.scss

@@ -6,6 +6,7 @@
 @import 'navbar';
 @import 'inputs';
 @import 'timeline';
+@import 'search';
 
 body {
     background-color: $bg_color;

+ 5 - 13
src/sass/profile/photo-rail.scss

@@ -16,13 +16,9 @@
     &-header-mobile {
         padding: 5px 12px 0;
         display: none;
-    }
-
-    &-label {
-        width: 100%;
+        width: calc(100% - 24px);
         float: unset;
         color: $accent;
-        display: flex;
         justify-content: space-between;
     }
 
@@ -57,13 +53,9 @@
     }
 }
 
-#photo-rail-toggle {
-    display: none;
-
-    &:checked ~ .photo-rail-grid {
-        max-height: 600px;
-        padding-bottom: 12px;
-    }
+@include create-toggle(photo-rail-grid, 640px);
+#photo-rail-grid-toggle:checked ~ .photo-rail-grid {
+    padding-bottom: 12px;
 }
 
 @media(max-width: 600px) {
@@ -72,7 +64,7 @@
     }
 
     .photo-rail-header-mobile {
-        display: block;
+        display: flex;
     }
 
     .photo-rail-grid {

+ 83 - 0
src/sass/search.scss

@@ -0,0 +1,83 @@
+@import '_variables';
+@import '_mixins';
+
+.search-title {
+    font-weight: bold;
+    display: inline-block;
+    margin-top: 4px;
+}
+
+.search-field {
+    display: flex;
+    flex-wrap: wrap;
+
+    button {
+        margin: 0 2px 2px 0;
+    }
+
+    .pref-input {
+        margin: 0 4px 2px 0;
+        flex-grow: 1;
+    }
+
+    input[type="text"] {
+        height: 20px;
+        width: calc(100% - 8px);
+    }
+
+    > label {
+        display: inline;
+        background-color: #121212;
+        color: #F8F8F2;
+        border: 1px solid #FF6C6091;
+        padding: 1px 6px 2px 6px;
+        font-size: 14px;
+        cursor: pointer;
+        margin-bottom: 2px;
+
+        @include input-colors;
+    }
+
+    @include create-toggle(search-panel, 140px);
+}
+
+.search-panel {
+    max-height: 0;
+    overflow: hidden;
+    transition: max-height 0.4s;
+
+    flex-grow: 1;
+    font-weight: initial;
+    text-align: left;
+
+    > div {
+        line-height: 1.7em;
+    }
+
+    .checkbox-container {
+        display: inline;
+        padding-right: unset;
+        margin-bottom: unset;
+        margin-left: 23px;
+    }
+
+    .checkbox {
+        right: unset;
+        left: -22px;
+    }
+
+    .checkbox-container .checkbox:after {
+        top: -4px;
+    }
+}
+
+.search-toggles {
+    flex-grow: 1;
+    display: grid;
+    grid-template-columns: repeat(6, auto);
+    grid-column-gap: 10px;
+}
+
+@include search-resize(530px, 5, 185px);
+@include search-resize(475px, 4, 185px);
+@include search-resize(406px, 3, 250px);

+ 1 - 11
src/sass/timeline.scss

@@ -10,21 +10,16 @@
     > div:not(:last-child) {
         border-bottom: 1px solid $border_grey;
     }
-
 }
 
 .timeline-header {
     background-color: $bg_panel;
     text-align: center;
-    padding: 10px;
+    padding: 8px;
     display: block;
     font-weight: bold;
     margin-bottom: 5px;
 
-    input[type="text"] {
-        height: 20px;
-    }
-
     button {
         float: unset;
     }
@@ -74,11 +69,6 @@
     padding: 6px 0;
 }
 
-.timeline-header {
-    background-color: $bg_panel;
-    padding: 6px 0;
-}
-
 .timeline-protected {
     text-align: center;
 

+ 1 - 1
src/types.nim

@@ -57,7 +57,7 @@ dbFromTypes("cache.db", "", "", "", [Profile, Video])
 
 type
   QueryKind* = enum
-    replies, media, multi, users, custom
+    posts, replies, media, multi, users, custom
 
   Query* = object
     kind*: QueryKind

+ 17 - 16
src/views/profile.nim

@@ -1,7 +1,7 @@
 import strutils, strformat
 import karax/[karaxdsl, vdom, vstyles]
 
-import tweet, timeline, renderutils
+import renderutils, search
 import ".."/[types, utils, formatters]
 
 proc renderStat(num, class: string; text=""): VNode =
@@ -54,11 +54,10 @@ proc renderPhotoRail(profile: Profile; photoRail: seq[GalleryPhoto]): VNode =
       a(href=(&"/{profile.username}/media")):
         icon "picture", $profile.media & " Photos and videos"
 
-    input(id="photo-rail-toggle", `type`="checkbox")
-    tdiv(class="photo-rail-header-mobile"):
-      label(`for`="photo-rail-toggle", class="photo-rail-label"):
-        icon "picture", $profile.media & " Photos and videos"
-        icon "down"
+    input(id="photo-rail-grid-toggle", `type`="checkbox")
+    label(`for`="photo-rail-grid-toggle", class="photo-rail-header-mobile"):
+      icon "picture", $profile.media & " Photos and videos"
+      icon "down"
 
     tdiv(class="photo-rail-grid"):
       for i, photo in photoRail:
@@ -76,13 +75,17 @@ proc renderBanner(profile: Profile): VNode =
         genImg(profile.banner)
 
 proc renderProtected(username: string): VNode =
-  buildHtml(tdiv(class="timeline-container timeline")):
-    tdiv(class="timeline-header timeline-protected"):
-      h2: text "This account's tweets are protected."
-      p: text &"Only confirmed followers have access to @{username}'s tweets."
+  buildHtml(tdiv(class="timeline-container")):
+    tdiv(class="timeline-container timeline"):
+      tdiv(class="timeline-header timeline-protected"):
+        h2: text "This account's tweets are protected."
+        p: text &"Only confirmed followers have access to @{username}'s tweets."
 
 proc renderProfile*(profile: Profile; timeline: Timeline;
                     photoRail: seq[GalleryPhoto]; prefs: Prefs; path: string): VNode =
+  if timeline.query.isNone:
+    timeline.query = some Query(fromUser: @[profile.username])
+
   buildHtml(tdiv(class="profile-tabs")):
     if not prefs.hideBanner:
       tdiv(class="profile-banner"):
@@ -94,9 +97,7 @@ proc renderProfile*(profile: Profile; timeline: Timeline;
       if photoRail.len > 0:
         renderPhotoRail(profile, photoRail)
 
-    tdiv(class="timeline-container"):
-      if profile.protected:
-        renderProtected(profile.username)
-      else:
-        renderProfileTabs(timeline.query, profile.username)
-        renderTimelineTweets(timeline, prefs, path)
+    if profile.protected:
+      renderProtected(profile.username)
+    else:
+      renderTweetSearch(timeline, prefs, path)

+ 1 - 3
src/views/renderutils.nim

@@ -59,8 +59,7 @@ proc buttonReferer*(action, text, path: string; class=""; `method`="post"): VNod
       text text
 
 proc genCheckbox*(pref, label: string; state: bool): VNode =
-  buildHtml(tdiv(class="pref-group")):
-    label(class="checkbox-container"):
+  buildHtml(label(class="pref-group checkbox-container")):
       text label
       if state: input(name=pref, `type`="checkbox", checked="")
       else: input(name=pref, `type`="checkbox")
@@ -83,4 +82,3 @@ proc genSelect*(pref, label, state: string; options: seq[string]): VNode =
           option(value=opt, selected=""): text opt
         else:
           option(value=opt): text opt
-

+ 81 - 47
src/views/search.nim

@@ -1,9 +1,24 @@
-import strutils, strformat, unicode
+import strutils, strformat, unicode, tables
 import karax/[karaxdsl, vdom, vstyles]
 
 import renderutils, timeline
 import ".."/[types, formatters, query]
 
+let toggles = {
+  "nativeretweets": "Retweets",
+  "media": "Media",
+  "videos": "Videos",
+  "news": "News",
+  "verified": "Verified",
+  "native_video": "Native videos",
+  "replies": "Replies",
+  "links": "Links",
+  "images": "Images",
+  "safe": "Safe",
+  "quote": "Quotes",
+  "pro_video": "Pro videos"
+}.toOrderedTable
+
 proc renderSearch*(): VNode =
   buildHtml(tdiv(class="panel-container")):
     tdiv(class="search-bar"):
@@ -12,55 +27,77 @@ proc renderSearch*(): VNode =
         input(`type`="text", name="text", autofocus="", placeholder="Enter username...")
         button(`type`="submit"): icon "search"
 
-proc renderTimelineSearch*(timeline: Timeline; prefs: Prefs; path: string): VNode =
-  let users =
-    if timeline.query.isSome: get(timeline.query).fromUser
-    else: @[]
+proc getTabClass(query: Option[Query]; tab: string): string =
+  var classes = @["tab-item"]
 
-  buildHtml(tdiv(class="timeline-container")):
-    tdiv(class="timeline-header"):
-      text users.join(" | ")
+  if query.isNone or get(query).kind == multi:
+    if tab == "posts":
+      classes.add "active"
+  elif $get(query).kind == tab:
+    classes.add "active"
+
+  return classes.join(" ")
+
+proc renderProfileTabs*(query: Option[Query]; username: string): VNode =
+  let link = "/" & username
+  buildHtml(ul(class="tab")):
+    li(class=query.getTabClass("posts")):
+      a(href=link): text "Tweets"
+    li(class=query.getTabClass("replies")):
+      a(href=(link & "/replies")): text "Tweets & Replies"
+    li(class=query.getTabClass("media")):
+      a(href=(link & "/media")): text "Media"
+    li(class=query.getTabClass("custom")):
+      a(href=(link & "/search")): text "Custom"
+
+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"
 
-    renderProfileTabs(timeline.query, users.join(","))
-    renderTimelineTweets(timeline, prefs, path)
+proc renderSearchPanel*(query: Query): VNode =
+  let user = query.fromUser.join(",")
+  let action = if user.len > 0: &"/{user}/search" else: "/search"
+  buildHtml(form(`method`="get", action=action, class="search-field")):
+    hiddenField("kind", "custom")
+    genInput("text", "", query.text, "Enter search...", class="pref-inline")
+    button(`type`="submit"): icon "search"
+    input(id="search-panel-toggle", `type`="checkbox")
+    label(`for`="search-panel-toggle"):
+      icon "down"
+    tdiv(class="search-panel"):
+      for f in @["filter", "exclude"]:
+        span(class="search-title"): text capitalize(f)
+        tdiv(class="search-toggles"):
+          for k, v in toggles:
+            let state =
+              if f == "filter": k in query.filters
+              else: k in query.excludes
+            genCheckbox(&"{f[0]}-{k}", v, state)
 
 proc renderTweetSearch*(tweets: Result[Tweet]; prefs: Prefs; path: string): VNode =
-  let query = if tweets.query.isSome: get(tweets.query) else: Query(kind: custom)
+  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)
+    if query.fromUser.len > 1:
+      tdiv(class="timeline-header"):
+        text query.fromUser.join(" | ")
+    if query.fromUser.len == 0 or query.kind == custom:
+      tdiv(class="timeline-header"):
+        renderSearchPanel(query)
+
+    if query.fromUser.len > 0:
+      renderProfileTabs(tweets.query, query.fromUser.join(","))
+    else:
+      renderSearchTabs(tweets.query)
+
     renderTimelineTweets(tweets, prefs, path)
 
 proc renderUserSearch*(users: Result[Profile]; prefs: Prefs): VNode =
@@ -74,9 +111,6 @@ proc renderUserSearch*(users: Result[Profile]; prefs: Prefs): VNode =
         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)

+ 0 - 32
src/views/timeline.nim

@@ -12,38 +12,6 @@ proc getQuery(query: Option[Query]): string =
     if result[^1] != '?':
       result &= "&"
 
-proc getTabClass(query: Option[Query]; tab: string): string =
-  var classes = @["tab-item"]
-
-  if query.isNone or get(query).kind == multi:
-    if tab == "posts":
-      classes.add "active"
-  elif $get(query).kind == tab:
-    classes.add "active"
-
-  return classes.join(" ")
-
-proc renderProfileTabs*(query: Option[Query]; username: string): VNode =
-  let link = "/" & username
-  buildHtml(ul(class="tab")):
-    li(class=query.getTabClass("posts")):
-      a(href=link): text "Tweets"
-    li(class=query.getTabClass("replies")):
-      a(href=(link & "/replies")): text "Tweets & Replies"
-    li(class=query.getTabClass("media")):
-      a(href=(link & "/media")): text "Media"
-
-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"
-
 proc renderNewer(query: Option[Query]): VNode =
   buildHtml(tdiv(class="timeline-item show-more")):
     a(href=(getQuery(query).strip(chars={'?', '&'}))):