diff --git a/public/css/style.css b/public/css/style.css index 01f2607..e72cfd8 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1204,6 +1204,19 @@ legend { display: inline; } +.icon-button button { + color: #ff6c60; + text-decoration: none; + border: none; + float: none; + padding: unset; + padding-left: 4px; +} + +.icon-button button:hover { + color: #ffaca0; +} + .checkbox-container { display: block; position: relative; diff --git a/src/nitter.nim b/src/nitter.nim index c35ebb7..d3ef970 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -11,7 +11,7 @@ const configPath {.strdefine.} = "./nitter.conf" let cfg = getConfig(configPath) proc showSingleTimeline(name, after, agent: string; query: Option[Query]; - prefs: Prefs): Future[string] {.async.} = + prefs: Prefs; path: string): Future[string] {.async.} = let railFut = getPhotoRail(name, agent) var timeline: Timeline @@ -36,11 +36,12 @@ proc showSingleTimeline(name, after, agent: string; query: Option[Query]; if profile.username.len == 0: return "" - let profileHtml = renderProfile(profile, timeline, await railFut, prefs) - return renderMain(profileHtml, prefs, cfg.title, pageTitle(profile), pageDesc(profile)) + let profileHtml = renderProfile(profile, timeline, await railFut, prefs, path) + return renderMain(profileHtml, prefs, cfg.title, pageTitle(profile), + pageDesc(profile), path) proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query]; - prefs: Prefs): Future[string] {.async.} = + prefs: Prefs; path: string): Future[string] {.async.} = var q = query if q.isSome: get(q).fromUser = names @@ -48,19 +49,19 @@ proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Q q = some(Query(kind: multi, fromUser: names, excludes: @["replies"])) var timeline = renderMulti(await getTimelineSearch(get(q), after, agent), - names.join(","), prefs) + names.join(","), prefs, path) return renderMain(timeline, prefs, cfg.title, "Multi") proc showTimeline(name, after: string; query: Option[Query]; - prefs: Prefs): Future[string] {.async.} = + prefs: Prefs; path: string): Future[string] {.async.} = let agent = getAgent() let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0) if names.len == 1: - return await showSingleTimeline(names[0], after, agent, query, prefs) + return await showSingleTimeline(names[0], after, agent, query, prefs, path) else: - return await showMultiTimeline(names, after, agent, query, prefs) + return await showMultiTimeline(names, after, agent, query, prefs, path) template respTimeline(timeline: typed) = if timeline.len == 0: @@ -70,6 +71,12 @@ template respTimeline(timeline: typed) = template cookiePrefs(): untyped {.dirty.} = getPrefs(request.cookies.getOrDefault("preferences")) +template getPath(): untyped {.dirty.} = + $(parseUri(request.path) ? filterParams(request.params)) + +template refPath(): untyped {.dirty.} = + if @"referer".len > 0: @"referer" else: "/" + setProfileCacheTime(cfg.profileCacheTime) settings: @@ -87,52 +94,53 @@ routes: redirect("/" & @"query") get "/settings": - let refUri = request.headers.getOrDefault("Referer").parseUri() - var path = - if refUri.path.len > 0 and "/settings" notin refUri.path: refUri.path - else: "/" - if refUri.query.len > 0: path &= &"?{refUri.query}" let prefs = cookiePrefs() - resp renderMain(renderPreferences(prefs, path), prefs, cfg.title, "Preferences") + let path = refPath() + resp renderMain(renderPreferences(prefs, path), prefs, cfg.title, + "Preferences", path) post "/saveprefs": var prefs = cookiePrefs() genUpdatePrefs() setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps) - redirect(decodeUrl(@"referer")) + redirect(refPath()) post "/resetprefs": var prefs = cookiePrefs() resetPrefs(prefs) setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps) - redirect("/settings") + redirect($(parseUri("/settings") ? filterParams(request.params))) post "/enablehls": var prefs = cookiePrefs() prefs.hlsPlayback = true cache(prefs) setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps) - redirect(request.headers.getOrDefault("referer")) + redirect(refPath()) get "/@name/?": cond '.' notin @"name" - respTimeline(await showTimeline(@"name", @"after", none(Query), cookiePrefs())) + respTimeline(await showTimeline(@"name", @"after", none(Query), + cookiePrefs(), getPath())) get "/@name/search": cond '.' notin @"name" let prefs = cookiePrefs() let query = initQuery(@"filter", @"include", @"not", @"sep", @"name") - respTimeline(await showTimeline(@"name", @"after", some(query), cookiePrefs())) + respTimeline(await showTimeline(@"name", @"after", some(query), + cookiePrefs(), getPath())) get "/@name/replies": cond '.' notin @"name" let prefs = cookiePrefs() - respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")), cookiePrefs())) + respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")), + cookiePrefs(), getPath())) get "/@name/media": cond '.' notin @"name" let prefs = cookiePrefs() - respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")), cookiePrefs())) + respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")), + cookiePrefs(), getPath())) get "/@name/status/@id": cond '.' notin @"name" @@ -142,22 +150,23 @@ routes: if conversation == nil or conversation.tweet.id.len == 0: resp Http404, showError("Tweet not found", cfg.title) + let path = getPath() let title = pageTitle(conversation.tweet.profile) let desc = conversation.tweet.text - let html = renderConversation(conversation, prefs) + let html = renderConversation(conversation, prefs, path) if conversation.tweet.video.isSome(): let thumb = get(conversation.tweet.video).thumb let vidUrl = getVideoEmbed(conversation.tweet.id) - resp renderMain(html, prefs, cfg.title, title, desc, images = @[thumb], + resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb], `type`="video", video=vidUrl) elif conversation.tweet.gif.isSome(): let thumb = get(conversation.tweet.gif).thumb let vidUrl = getVideoEmbed(conversation.tweet.id) - resp renderMain(html, prefs, cfg.title, title, desc, images = @[thumb], + resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb], `type`="video", video=vidUrl) else: - resp renderMain(html, prefs, cfg.title, title, desc, images=conversation.tweet.photos) + resp renderMain(html, prefs, cfg.title, title, desc, path, images=conversation.tweet.photos) get "/i/web/status/@id": redirect("/i/status/" & @"id") diff --git a/src/utils.nim b/src/utils.nim index 8afa3a1..28008c6 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -1,4 +1,4 @@ -import strutils, strformat, uri +import strutils, strformat, sequtils, uri, tables import nimcrypto, regex const key = "supersecretkey" @@ -25,3 +25,7 @@ proc getSigUrl*(link: string; path: string): string = proc cleanFilename*(filename: string): string = const reg = re"[^A-Za-z0-9._-]" filename.replace(reg, "_") + +proc filterParams*(params: Table): seq[(string, string)] = + let filter = ["name", "id"] + toSeq(params.pairs()).filterIt(it[0] notin filter) diff --git a/src/views/general.nim b/src/views/general.nim index bc032ed..92c060a 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -5,7 +5,7 @@ import ../utils, ../types const doctype = "\n" -proc renderNavbar*(title: string): VNode = +proc renderNavbar*(title, path: string): VNode = buildHtml(nav(id="nav", class="nav-bar container")): tdiv(class="inner-nav"): tdiv(class="item"): @@ -15,10 +15,10 @@ proc renderNavbar*(title: string): VNode = tdiv(class="item right"): icon "info-circled", title="About", href="/about" - icon "cog", title="Preferences", href="/settings" + iconReferer "cog", "/settings", path, title="Preferences" proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=""; - `type`="article"; video=""; images: seq[string] = @[]): string = + path="/"; `type`="article"; video=""; images: seq[string] = @[]): string = let node = buildHtml(html(lang="en")): head: link(rel="stylesheet", `type`="text/css", href="/css/style.css") @@ -47,7 +47,7 @@ proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=" meta(property="og:video:secure_url", content=video) body: - renderNavbar(title) + renderNavbar(title, path) tdiv(id="content", class="container"): body diff --git a/src/views/preferences.nim b/src/views/preferences.nim index 14e44dd..8a1fe5f 100644 --- a/src/views/preferences.nim +++ b/src/views/preferences.nim @@ -63,5 +63,6 @@ proc renderPreferences*(prefs: Prefs; path: string): VNode = text "Save preferences" form(`method`="post", action="/resetprefs", class="pref-reset"): + verbatim "" % path button(`type`="submit"): text "Reset preferences" diff --git a/src/views/profile.nim b/src/views/profile.nim index 43cbbd0..eac2d87 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -68,7 +68,7 @@ proc renderBanner(profile: Profile): VNode = genImg(profile.banner) proc renderProfile*(profile: Profile; timeline: Timeline; - photoRail: seq[GalleryPhoto]; prefs: Prefs): VNode = + photoRail: seq[GalleryPhoto]; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="profile-tabs")): if not prefs.hideBanner: tdiv(class="profile-banner"): @@ -81,9 +81,10 @@ proc renderProfile*(profile: Profile; timeline: Timeline; renderPhotoRail(profile, photoRail) tdiv(class="timeline-tab"): - renderTimeline(timeline, profile.username, profile.protected, prefs) + renderTimeline(timeline, profile.username, profile.protected, prefs, path) -proc renderMulti*(timeline: Timeline; usernames: string; prefs: Prefs): VNode = +proc renderMulti*(timeline: Timeline; usernames: string; + prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="multi-timeline")): tdiv(class="timeline-tab"): - renderTimeline(timeline, usernames, false, prefs, multi=true) + renderTimeline(timeline, usernames, false, prefs, path, multi=true) diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index c42c76e..24a42a5 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -1,3 +1,4 @@ +import strutils import karax/[karaxdsl, vdom, vstyles] import ../types, ../utils @@ -37,3 +38,9 @@ proc linkText*(text: string; class=""): VNode = let url = if "http" notin text: "http://" & text else: text buildHtml(): a(href=url, class=class): text text + +proc iconReferer*(icon, action, path: string, title=""): VNode = + buildHtml(form(`method`="get", action=action, class="icon-button")): + verbatim "" % path + button(`type`="submit"): + icon icon, title=title diff --git a/src/views/status.nim b/src/views/status.nim index aa109ba..24dac1e 100644 --- a/src/views/status.nim +++ b/src/views/status.nim @@ -11,34 +11,34 @@ proc renderMoreReplies(thread: Thread): VNode = a(class="more-replies-text", title="Not implemented yet"): text $num & "more " & reply -proc renderReplyThread(thread: Thread; prefs: Prefs): VNode = +proc renderReplyThread(thread: Thread; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="reply thread thread-line")): for i, tweet in thread.content: let last = (i == thread.content.high and thread.more == 0) - renderTweet(tweet, prefs, index=i, last=last) + renderTweet(tweet, prefs, path, index=i, last=last) if thread.more != 0: renderMoreReplies(thread) -proc renderConversation*(conversation: Conversation; prefs: Prefs): VNode = +proc renderConversation*(conversation: Conversation; prefs: Prefs; path: string): VNode = let hasAfter = conversation.after != nil buildHtml(tdiv(class="conversation", id="posts")): tdiv(class="main-thread"): if conversation.before != nil: tdiv(class="before-tweet thread-line"): for i, tweet in conversation.before.content: - renderTweet(tweet, prefs, index=i) + renderTweet(tweet, prefs, path, index=i) tdiv(class="main-tweet"): let afterClass = if hasAfter: "thread thread-line" else: "" - renderTweet(conversation.tweet, prefs, class=afterClass) + renderTweet(conversation.tweet, prefs, path, class=afterClass) if hasAfter: tdiv(class="after-tweet thread-line"): let total = conversation.after.content.high let more = conversation.after.more for i, tweet in conversation.after.content: - renderTweet(tweet, prefs, index=i, last=(i == total and more == 0)) + renderTweet(tweet, prefs, path, index=i, last=(i == total and more == 0)) if more != 0: renderMoreReplies(conversation.after) @@ -46,4 +46,5 @@ proc renderConversation*(conversation: Conversation; prefs: Prefs): VNode = if conversation.replies.len > 0: tdiv(class="replies"): for thread in conversation.replies: - renderReplyThread(thread, prefs) + if thread == nil: continue + renderReplyThread(thread, prefs, path) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index ef8c0bd..01b7d3e 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -54,28 +54,29 @@ proc renderProtected(username: string): VNode = h2: text "This account's tweets are protected." p: text &"Only confirmed followers have access to @{username}'s tweets." -proc renderThread(thread: seq[Tweet]; prefs: Prefs): VNode = +proc renderThread(thread: seq[Tweet]; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="timeline-tweet thread-line")): for i, threadTweet in thread.sortedByIt(it.time): - renderTweet(threadTweet, prefs, class="thread", index=i, total=thread.high) + renderTweet(threadTweet, prefs, path, class="thread", + index=i, total=thread.high) proc threadFilter(it: Tweet; tweetThread: string): bool = it.retweet.isNone and it.reply.len == 0 and it.threadId == tweetThread -proc renderTweets(timeline: Timeline; prefs: Prefs): VNode = +proc renderTweets(timeline: Timeline; prefs: Prefs; path: string): VNode = buildHtml(tdiv(id="posts")): var threads: seq[string] for tweet in timeline.content: if tweet.threadId in threads: continue let thread = timeline.content.filterIt(threadFilter(it, tweet.threadId)) if thread.len < 2: - renderTweet(tweet, prefs, class="timeline-tweet") + renderTweet(tweet, prefs, path, class="timeline-tweet") else: - renderThread(thread, prefs) + renderThread(thread, prefs, path) threads &= tweet.threadId proc renderTimeline*(timeline: Timeline; username: string; protected: bool; - prefs: Prefs; multi=false): VNode = + prefs: Prefs; path: string; multi=false): VNode = buildHtml(tdiv): if multi: tdiv(class="multi-header"): @@ -91,7 +92,7 @@ proc renderTimeline*(timeline: Timeline; username: string; protected: bool; elif timeline.content.len == 0: renderNoneFound() else: - renderTweets(timeline, prefs) + renderTweets(timeline, prefs, path) if timeline.hasMore or timeline.query.isSome: renderOlder(timeline, username) else: diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 6f5ba5f..47ee506 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -50,7 +50,7 @@ proc isPlaybackEnabled(prefs: Prefs; video: Video): bool = of mp4: prefs.mp4Playback of m3u8, vmap: prefs.hlsPlayback -proc renderVideoDisabled(video: Video): VNode = +proc renderVideoDisabled(video: Video; path: string): VNode = buildHtml(tdiv): img(src=video.thumb.getSigUrl("pic")) tdiv(class="video-overlay"): @@ -59,6 +59,7 @@ proc renderVideoDisabled(video: Video): VNode = p: text "mp4 playback disabled in preferences" of m3u8, vmap: form(`method`="post", action=("/enablehls")): + verbatim "" % path button(`type`="submit"): text "Enable hls playback" @@ -72,7 +73,7 @@ proc renderVideoUnavailable(video: Video): VNode = else: p: text "This media is unavailable" -proc renderVideo(video: Video; prefs: Prefs): VNode = +proc renderVideo(video: Video; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="attachments")): tdiv(class="gallery-video"): tdiv(class="attachment video-container"): @@ -80,7 +81,7 @@ proc renderVideo(video: Video; prefs: Prefs): VNode = if not video.available: renderVideoUnavailable(video) elif not prefs.isPlaybackEnabled(video): - renderVideoDisabled(video) + renderVideoDisabled(video, path) else: let source = video.url.getSigUrl("video") case video.playbackType @@ -137,7 +138,7 @@ proc renderCardContent(card: Card): VNode = p(class="card-description"): text card.text span(class="card-destination"): text card.dest -proc renderCard(card: Card; prefs: Prefs): VNode = +proc renderCard(card: Card; prefs: Prefs; path: string): VNode = const largeCards = {summaryLarge, liveEvent, promoWebsite, promoVideo} let large = if card.kind in largeCards: " large" else: "" let url = replaceUrl(card.url, prefs) @@ -145,7 +146,7 @@ proc renderCard(card: Card; prefs: Prefs): VNode = buildHtml(tdiv(class=("card" & large))): if card.video.isSome: tdiv(class="card-container"): - renderVideo(get(card.video), prefs) + renderVideo(get(card.video), prefs, path) a(class="card-content-container", href=url): renderCardContent(card) else: @@ -215,7 +216,7 @@ proc renderQuote(quote: Quote; prefs: Prefs): VNode = a(class="show-thread", href=getLink(quote)): text "Show this thread" -proc renderTweet*(tweet: Tweet; prefs: Prefs; class=""; +proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; total=(-1); last=false): VNode = var divClass = class if index == total or last: @@ -243,11 +244,11 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; class=""; renderQuote(tweet.quote.get(), prefs) if tweet.card.isSome: - renderCard(tweet.card.get(), prefs) + renderCard(tweet.card.get(), prefs, path) elif tweet.photos.len > 0: renderAlbum(tweet) elif tweet.video.isSome: - renderVideo(tweet.video.get(), prefs) + renderVideo(tweet.video.get(), prefs, path) views = tweet.video.get().views elif tweet.gif.isSome: renderGif(tweet.gif.get(), prefs)