Redesign and fix search, add custom timeline tab
This commit is contained in:
		
							parent
							
								
									c1a136c6db
								
							
						
					
					
						commit
						7d7eb085ca
					
				| 
						 | 
				
			
			@ -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("&")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@
 | 
			
		|||
@import 'navbar';
 | 
			
		||||
@import 'inputs';
 | 
			
		||||
@import 'timeline';
 | 
			
		||||
@import 'search';
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    background-color: $bg_color;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
 | 
			
		||||
    renderProfileTabs(timeline.query, users.join(","))
 | 
			
		||||
    renderTimelineTweets(timeline, prefs, path)
 | 
			
		||||
  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"
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
          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)
 | 
			
		||||
    if query.fromUser.len > 0:
 | 
			
		||||
      renderProfileTabs(tweets.query, query.fromUser.join(","))
 | 
			
		||||
    else:
 | 
			
		||||
      renderSearchTabs(tweets.query)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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={'?', '&'}))):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue