コミットを比較
21 コミット
c5587a922e
...
9a1c51a131
作成者 | SHA1 | 日付 |
---|---|---|
テクニカル諏訪子 | 9a1c51a131 | |
Zed | bc352cdb65 | |
Zed | 47ed1a3ae8 | |
Zed | bb981df657 | |
Zed | ab51ff06c8 | |
Mitarashi | b14fb0162f | |
Zed | 47f47bb907 | |
Zed | 74c4377198 | |
Zed | a9034928eb | |
Zed | 9dd7419ecf | |
Zed | 9d117aa15b | |
Zed | 19a89b79f5 | |
Zed | 1ce6ff2b2f | |
Zed | b8a3ffb0c4 | |
Zed | aed31b2269 | |
Zed | 5501752fdb | |
Zed | 47ec6ff3d2 | |
jackyzy823 | a25bd0855b | |
jackyzy823 | ef7ad67674 | |
jackyzy823 | db090faf36 | |
jackyzy823 | 35bb5f9132 |
|
@ -1,4 +1,3 @@
|
|||
.*
|
||||
*.png
|
||||
*.md
|
||||
LICENSE
|
||||
|
|
|
@ -107,6 +107,8 @@ performance reasons.
|
|||
|
||||
### Docker
|
||||
|
||||
#### NOTE: For ARM64/ARM support, please use [unixfox's image](https://quay.io/repository/unixfox/nitter?tab=tags), more info [here](https://github.com/zedeus/nitter/issues/399#issuecomment-997263495)
|
||||
|
||||
To run Nitter with Docker, you'll need to install and run Redis separately
|
||||
before you can run the container. See below for how to also run Redis using
|
||||
Docker.
|
||||
|
|
|
@ -31,7 +31,7 @@ proc windows(): string =
|
|||
trident = ["", "; Trident/5.0", "; Trident/6.0", "; Trident/7.0"]
|
||||
"Windows " & sample(nt) & sample(enc) & sample(arch) & sample(trident)
|
||||
|
||||
let macs = toSeq(6..15).mapIt($it) & @["14_4", "10_1", "9_3"]
|
||||
const macs = toSeq(6..15).mapIt($it) & @["14_4", "10_1", "9_3"]
|
||||
|
||||
proc mac(): string =
|
||||
"Macintosh; Intel Mac OS X 10_" & sample(macs) & sample(enc)
|
||||
|
|
|
@ -9,13 +9,13 @@ proc getGraphProfile*(username: string): Future[Profile] {.async.} =
|
|||
js = await fetch(graphUser ? {"variables": $variables})
|
||||
result = parseGraphProfile(js, username)
|
||||
|
||||
proc getGraphList*(name, list: string): Future[List] {.async.} =
|
||||
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
|
||||
let
|
||||
variables = %*{"screenName": name, "listSlug": list, "withHighlightedLabel": false}
|
||||
js = await fetch(graphList ? {"variables": $variables})
|
||||
result = parseGraphList(js)
|
||||
|
||||
proc getGraphListById*(id: string): Future[List] {.async.} =
|
||||
proc getGraphList*(id: string): Future[List] {.async.} =
|
||||
let
|
||||
variables = %*{"listId": id, "withHighlightedLabel": false}
|
||||
js = await fetch(graphListId ? {"variables": $variables})
|
||||
|
|
|
@ -5,7 +5,7 @@ import types, tokens, consts, parserutils, http_pool
|
|||
|
||||
const rl = "x-rate-limit-"
|
||||
|
||||
var pool {.threadvar.}: HttpPool
|
||||
var pool: HttpPool
|
||||
|
||||
proc genParams*(pars: openarray[(string, string)] = @[]; cursor="";
|
||||
count="20"; ext=true): seq[(string, string)] =
|
||||
|
|
|
@ -5,8 +5,8 @@ type
|
|||
HttpPool* = ref object
|
||||
conns*: seq[AsyncHttpClient]
|
||||
|
||||
var maxConns {.threadvar.}: int
|
||||
var proxy {.threadvar.}: Proxy
|
||||
var maxConns: int
|
||||
var proxy: Proxy
|
||||
|
||||
proc setMaxHttpConns*(n: int) =
|
||||
maxConns = n
|
||||
|
|
|
@ -73,9 +73,9 @@ proc parseGraphList*(js: JsonNode): List =
|
|||
|
||||
result = List(
|
||||
id: list{"id_str"}.getStr,
|
||||
name: list{"name"}.getStr.replace(' ', '-'),
|
||||
name: list{"name"}.getStr,
|
||||
username: list{"user", "legacy", "screen_name"}.getStr,
|
||||
userId: list{"user", "legacy", "id_str"}.getStr,
|
||||
userId: list{"user", "rest_id"}.getStr,
|
||||
description: list{"description"}.getStr,
|
||||
members: list{"member_count"}.getInt,
|
||||
banner: list{"custom_banner_media", "media_info", "url"}.getImageStr
|
||||
|
@ -128,13 +128,16 @@ proc parseVideo(js: JsonNode): Video =
|
|||
views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr,
|
||||
available: js{"ext_media_availability", "status"}.getStr == "available",
|
||||
title: js{"ext_alt_text"}.getStr,
|
||||
durationMs: js{"duration_millis"}.getInt
|
||||
durationMs: js{"video_info", "duration_millis"}.getInt
|
||||
# playbackType: mp4
|
||||
)
|
||||
|
||||
with title, js{"additional_media_info", "title"}:
|
||||
result.title = title.getStr
|
||||
|
||||
with description, js{"additional_media_info", "description"}:
|
||||
result.description = description.getStr
|
||||
|
||||
for v in js{"video_info", "variants"}:
|
||||
result.variants.add VideoVariant(
|
||||
videoType: parseEnum[VideoType](v{"content_type"}.getStr("summary")),
|
||||
|
|
|
@ -118,7 +118,7 @@ proc getBanner*(js: JsonNode): string =
|
|||
if color.len > 0:
|
||||
return '#' & color
|
||||
|
||||
# use primary color from profile picture color histrogram
|
||||
# use primary color from profile picture color histogram
|
||||
with p, js{"profile_image_extensions", "mediaColor", "r", "ok", "palette"}:
|
||||
if p.len > 0:
|
||||
let pal = p[0]{"rgb"}
|
||||
|
|
|
@ -6,7 +6,7 @@ from parsecfg import nil
|
|||
|
||||
export genUpdatePrefs, genResetPrefs
|
||||
|
||||
var defaultPrefs* {.threadvar.}: Prefs
|
||||
var defaultPrefs*: Prefs
|
||||
|
||||
proc updateDefaultPrefs*(cfg: parsecfg.Config) =
|
||||
genDefaultPrefs()
|
||||
|
|
|
@ -4,11 +4,12 @@ import redis, redpool, flatty, supersnappy
|
|||
|
||||
import types, api
|
||||
|
||||
const redisNil = "\0\0"
|
||||
const
|
||||
redisNil = "\0\0"
|
||||
baseCacheTime = 60 * 60
|
||||
|
||||
var
|
||||
pool {.threadvar.}: RedisPool
|
||||
baseCacheTime = 60 * 60
|
||||
pool: RedisPool
|
||||
rssCacheTime: int
|
||||
listCacheTime*: int
|
||||
|
||||
|
@ -17,7 +18,9 @@ proc toFlatty*(s: var string, x: DateTime) =
|
|||
s.toFlatty(x.toTime().toUnix())
|
||||
|
||||
proc fromFlatty*(s: string, i: var int, x: var DateTime) =
|
||||
x = fromUnix(s.fromFlatty(int64)).utc()
|
||||
var unix: int64
|
||||
s.fromFlatty(i, unix)
|
||||
x = fromUnix(unix).utc()
|
||||
|
||||
proc setCacheTimes*(cfg: Config) =
|
||||
rssCacheTime = cfg.rssCacheTime * 60
|
||||
|
@ -56,7 +59,7 @@ proc initRedisPool*(cfg: Config) {.async.} =
|
|||
|
||||
template pidKey(name: string): string = "pid:" & $(hash(name) div 1_000_000)
|
||||
template profileKey(name: string): string = "p:" & name
|
||||
template listKey(l: List): string = toLower("l:" & l.username & '/' & l.name)
|
||||
template listKey(l: List): string = "l:" & l.id
|
||||
|
||||
proc get(query: string): Future[string] {.async.} =
|
||||
pool.withAcquire(r):
|
||||
|
@ -129,17 +132,17 @@ proc getCachedPhotoRail*(name: string): Future[PhotoRail] {.async.} =
|
|||
result = await getPhotoRail(name)
|
||||
await cache(result, name)
|
||||
|
||||
proc getCachedList*(username=""; name=""; id=""): Future[List] {.async.} =
|
||||
let list = if id.len > 0: redisNil
|
||||
else: await get(toLower("l:" & username & '/' & name))
|
||||
proc getCachedList*(username=""; slug=""; id=""): Future[List] {.async.} =
|
||||
let list = if id.len == 0: redisNil
|
||||
else: await get("l:" & id)
|
||||
|
||||
if list != redisNil:
|
||||
result = fromFlatty(uncompress(list), List)
|
||||
else:
|
||||
if id.len > 0:
|
||||
result = await getGraphListById(id)
|
||||
result = await getGraphList(id)
|
||||
else:
|
||||
result = await getGraphList(username, name)
|
||||
result = await getGraphListBySlug(username, slug)
|
||||
await cache(result)
|
||||
|
||||
proc getCachedRss*(key: string): Future[Rss] {.async.} =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import strutils
|
||||
import strutils, uri
|
||||
|
||||
import jester
|
||||
|
||||
|
@ -8,41 +8,44 @@ import ".."/[types, redis_cache, api]
|
|||
import ../views/[general, timeline, list]
|
||||
export getListTimeline, getGraphList
|
||||
|
||||
template respList*(list, timeline, vnode: typed) =
|
||||
if list.id.len == 0:
|
||||
resp Http404, showError("List \"" & @"list" & "\" not found", cfg)
|
||||
template respList*(list, timeline, title, vnode: typed) =
|
||||
if list.id.len == 0 or list.name.len == 0:
|
||||
resp Http404, showError("List " & @"id" & " not found", cfg)
|
||||
|
||||
let
|
||||
html = renderList(vnode, timeline.query, list)
|
||||
rss = "/$1/lists/$2/rss" % [@"name", @"list"]
|
||||
rss = "/i/lists/$1/rss" % [@"id"]
|
||||
|
||||
resp renderMain(html, request, cfg, prefs, rss=rss, banner=list.banner)
|
||||
resp renderMain(html, request, cfg, prefs, titleText=title, rss=rss, banner=list.banner)
|
||||
|
||||
proc createListRouter*(cfg: Config) =
|
||||
router list:
|
||||
get "/@name/lists/@list/?":
|
||||
get "/@name/lists/@slug/?":
|
||||
cond '.' notin @"name"
|
||||
cond @"name" != "i"
|
||||
cond @"slug" != "memberships"
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
list = await getCachedList(@"name", @"list")
|
||||
timeline = await getListTimeline(list.id, getCursor())
|
||||
vnode = renderTimelineTweets(timeline, prefs, request.path)
|
||||
respList(list, timeline, vnode)
|
||||
|
||||
get "/@name/lists/@list/members":
|
||||
cond '.' notin @"name"
|
||||
cond @"name" != "i"
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
list = await getCachedList(@"name", @"list")
|
||||
members = await getListMembers(list, getCursor())
|
||||
respList(list, members, renderTimelineUsers(members, prefs, request.path))
|
||||
slug = decodeUrl(@"slug")
|
||||
list = await getCachedList(@"name", slug)
|
||||
if list.id.len == 0:
|
||||
resp Http404, showError("List \"" & @"slug" & "\" not found", cfg)
|
||||
redirect("/i/lists/" & list.id)
|
||||
|
||||
get "/i/lists/@id/?":
|
||||
cond '.' notin @"id"
|
||||
let list = await getCachedList(id=(@"id"))
|
||||
if list.id.len == 0:
|
||||
resp Http404
|
||||
await cache(list)
|
||||
redirect("/" & list.username & "/lists/" & list.name)
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
list = await getCachedList(id=(@"id"))
|
||||
title = "@" & list.username & "/" & list.name
|
||||
timeline = await getListTimeline(list.id, getCursor())
|
||||
vnode = renderTimelineTweets(timeline, prefs, request.path)
|
||||
respList(list, timeline, title, vnode)
|
||||
|
||||
get "/i/lists/@id/members":
|
||||
cond '.' notin @"id"
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
list = await getCachedList(id=(@"id"))
|
||||
title = "@" & list.username & "/" & list.name
|
||||
members = await getListMembers(list, getCursor())
|
||||
respList(list, members, title, renderTimelineUsers(members, prefs, request.path))
|
||||
|
|
|
@ -32,7 +32,7 @@ proc createPrefRouter*(cfg: Config) =
|
|||
|
||||
post "/resetprefs":
|
||||
genResetPrefs()
|
||||
redirect($(parseUri("/settings") ? filterParams(request.params)))
|
||||
redirect("/settings?referer=" & encodeUrl(refPath()))
|
||||
|
||||
post "/enablehls":
|
||||
savePref("hlsPlayback", "on", request)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import asyncdispatch, strutils, tables, times, hashes, supersnappy
|
||||
import asyncdispatch, strutils, tables, times, hashes, uri
|
||||
|
||||
import jester
|
||||
import jester, supersnappy
|
||||
|
||||
import router_utils, timeline
|
||||
import ../query
|
||||
|
@ -36,12 +36,18 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
|
|||
return Rss(feed: profile.username, cursor: "suspended")
|
||||
|
||||
if profile.fullname.len > 0:
|
||||
let rss = compress renderTimelineRss(timeline, profile, cfg, multi=(names.len > 1))
|
||||
let rss = compress renderTimelineRss(timeline, profile, cfg,
|
||||
multi=(names.len > 1))
|
||||
return Rss(feed: rss, cursor: timeline.bottom)
|
||||
|
||||
template respRss*(rss) =
|
||||
template respRss*(rss, page) =
|
||||
if rss.cursor.len == 0:
|
||||
resp Http404, showError("User \"" & @"name" & "\" not found", cfg)
|
||||
let info = case page
|
||||
of "User": " \"$1\" " % @"name"
|
||||
of "List": " $1 " % @"id"
|
||||
else: " "
|
||||
|
||||
resp Http404, showError(page & info & "not found", cfg)
|
||||
elif rss.cursor.len == 9 and rss.cursor == "suspended":
|
||||
resp Http404, showError(getSuspended(rss.feed), cfg)
|
||||
|
||||
|
@ -62,11 +68,11 @@ proc createRssRouter*(cfg: Config) =
|
|||
|
||||
let
|
||||
cursor = getCursor()
|
||||
key = $hash(genQueryUrl(query)) & cursor
|
||||
key = "search:" & $hash(genQueryUrl(query)) & ":" & cursor
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
respRss(rss)
|
||||
respRss(rss, "Search")
|
||||
|
||||
let tweets = await getSearch[Tweet](query, cursor)
|
||||
rss.cursor = tweets.bottom
|
||||
|
@ -74,7 +80,7 @@ proc createRssRouter*(cfg: Config) =
|
|||
genQueryUrl(query), cfg)
|
||||
|
||||
await cacheRss(key, rss)
|
||||
respRss(rss)
|
||||
respRss(rss, "Search")
|
||||
|
||||
get "/@name/rss":
|
||||
cond cfg.enableRss
|
||||
|
@ -82,16 +88,16 @@ proc createRssRouter*(cfg: Config) =
|
|||
let
|
||||
cursor = getCursor()
|
||||
name = @"name"
|
||||
key = name & cursor
|
||||
key = "twitter:" & name & ":" & cursor
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
respRss(rss)
|
||||
respRss(rss, "User")
|
||||
|
||||
rss = await timelineRss(request, cfg, Query(fromUser: @[name]))
|
||||
|
||||
await cacheRss(key, rss)
|
||||
respRss(rss)
|
||||
respRss(rss, "User")
|
||||
|
||||
get "/@name/@tab/rss":
|
||||
cond cfg.enableRss
|
||||
|
@ -105,36 +111,54 @@ proc createRssRouter*(cfg: Config) =
|
|||
of "search": initQuery(params(request), name=name)
|
||||
else: Query(fromUser: @[name])
|
||||
|
||||
var key = @"name" & "/" & @"tab"
|
||||
var key = @"tab" & ":" & @"name" & ":"
|
||||
if @"tab" == "search":
|
||||
key &= $hash(genQueryUrl(query))
|
||||
key &= $hash(genQueryUrl(query)) & ":"
|
||||
key &= getCursor()
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
respRss(rss)
|
||||
respRss(rss, "User")
|
||||
|
||||
rss = await timelineRss(request, cfg, query)
|
||||
|
||||
await cacheRss(key, rss)
|
||||
respRss(rss)
|
||||
respRss(rss, "User")
|
||||
|
||||
get "/@name/lists/@list/rss":
|
||||
get "/@name/lists/@slug/rss":
|
||||
cond cfg.enableRss
|
||||
cond @"name" != "i"
|
||||
let
|
||||
slug = decodeUrl(@"slug")
|
||||
list = await getCachedList(@"name", slug)
|
||||
cursor = getCursor()
|
||||
|
||||
if list.id.len == 0:
|
||||
resp Http404, showError("List \"" & @"slug" & "\" not found", cfg)
|
||||
|
||||
let url = "/i/lists/" & list.id & "/rss"
|
||||
if cursor.len > 0:
|
||||
redirect(url & "?cursor=" & encodeUrl(cursor, false))
|
||||
else:
|
||||
redirect(url)
|
||||
|
||||
get "/i/lists/@id/rss":
|
||||
cond cfg.enableRss
|
||||
cond '.' notin @"name"
|
||||
let
|
||||
cursor = getCursor()
|
||||
key = @"name" & "/" & @"list" & cursor
|
||||
key =
|
||||
if cursor.len == 0: "lists:" & @"id"
|
||||
else: "lists:" & @"id" & ":" & cursor
|
||||
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
respRss(rss)
|
||||
respRss(rss, "List")
|
||||
|
||||
let
|
||||
list = await getCachedList(@"name", @"list")
|
||||
list = await getCachedList(id=(@"id"))
|
||||
timeline = await getListTimeline(list.id, cursor)
|
||||
rss.cursor = timeline.bottom
|
||||
rss.feed = compress renderListRss(timeline.content, list, cfg)
|
||||
|
||||
await cacheRss(key, rss)
|
||||
respRss(rss)
|
||||
respRss(rss, "List")
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
@include ellipsis;
|
||||
white-space: unset;
|
||||
font-weight: bold;
|
||||
font-size: 1.15em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
@import '_mixins';
|
||||
|
||||
video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.gallery-video {
|
||||
|
@ -18,10 +18,13 @@ video {
|
|||
.video-container {
|
||||
max-height: 530px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ const
|
|||
failDelay = initDuration(minutes=30)
|
||||
|
||||
var
|
||||
clientPool {.threadvar.}: HttpPool
|
||||
tokenPool {.threadvar.}: seq[Token]
|
||||
clientPool: HttpPool
|
||||
tokenPool: seq[Token]
|
||||
lastFailed: Time
|
||||
|
||||
proc getPoolInfo*: string =
|
||||
|
|
|
@ -128,7 +128,7 @@ type
|
|||
videoDirectMessage = "video_direct_message"
|
||||
imageDirectMessage = "image_direct_message"
|
||||
audiospace = "audiospace"
|
||||
newsletter_publication = "newsletter_publication"
|
||||
newsletterPublication = "newsletter_publication"
|
||||
unknown
|
||||
|
||||
Card* = object
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
import strutils, strformat, sequtils, uri, tables, base64
|
||||
import strutils, strformat, uri, tables, base64
|
||||
import nimcrypto, regex
|
||||
|
||||
var
|
||||
hmacKey {.threadvar.}: string
|
||||
hmacKey: string
|
||||
base64Media = false
|
||||
|
||||
const
|
||||
https* = "https://"
|
||||
twimg* = "pbs.twimg.com/"
|
||||
badJpgExts = @["1500x500", "jpgn", "jpg:", "jpg_", "_jpg"]
|
||||
badPngExts = @["pngn", "png:", "png_", "_png"]
|
||||
nitterParams = ["name", "tab", "id", "list", "referer", "scroll"]
|
||||
twitterDomains = @[
|
||||
"twitter.com",
|
||||
"pic.twitter.com",
|
||||
|
@ -43,18 +42,9 @@ proc getPicUrl*(link: string): string =
|
|||
else:
|
||||
&"/pic/{encodeUrl(link)}"
|
||||
|
||||
proc cleanFilename*(filename: string): string =
|
||||
const reg = re"[^A-Za-z0-9._-]"
|
||||
result = filename.replace(reg, "_")
|
||||
if badJpgExts.anyIt(it in result):
|
||||
result &= ".jpg"
|
||||
elif badPngExts.anyIt(it in result):
|
||||
result &= ".png"
|
||||
|
||||
proc filterParams*(params: Table): seq[(string, string)] =
|
||||
const filter = ["name", "tab", "id", "list", "referer", "scroll"]
|
||||
for p in params.pairs():
|
||||
if p[1].len > 0 and p[0] notin filter:
|
||||
if p[1].len > 0 and p[0] notin nitterParams:
|
||||
result.add p
|
||||
|
||||
proc isTwitterUrl*(uri: Uri): bool =
|
||||
|
|
|
@ -12,8 +12,10 @@ const
|
|||
lp = readFile("public/lp.svg")
|
||||
|
||||
proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
|
||||
var path = $(parseUri(req.path) ? filterParams(req.params))
|
||||
if "/status" in path: path.add "#m"
|
||||
var path = req.params.getOrDefault("referer")
|
||||
if path.len == 0:
|
||||
path = $(parseUri(req.path) ? filterParams(req.params))
|
||||
if "/status/" in path: path.add "#m"
|
||||
|
||||
buildHtml(nav):
|
||||
tdiv(class="inner-nav"):
|
||||
|
@ -29,7 +31,7 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
|
|||
icon "bird", title="Open in Twitter", href=canonical
|
||||
a(href="https://liberapay.com/zedeus"): verbatim lp
|
||||
icon "info", title="About", href="/about"
|
||||
iconReferer "cog", "/settings", path, title="Preferences"
|
||||
icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path))
|
||||
|
||||
proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
|
||||
images: seq[string] = @[]; banner=""; ogTitle=""; theme="";
|
||||
|
@ -43,7 +45,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
|
|||
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
||||
|
||||
buildHtml(head):
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=6")
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=7")
|
||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")
|
||||
|
||||
if theme.len > 0:
|
||||
|
@ -86,7 +88,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
|
|||
|
||||
if banner.len > 0:
|
||||
let bannerUrl = getPicUrl(banner)
|
||||
link(rel="preload", type="image/png", href=getPicUrl(banner), `as`="image")
|
||||
link(rel="preload", type="image/png", href=bannerUrl, `as`="image")
|
||||
|
||||
for url in images:
|
||||
let suffix = if "400x400" in url: "" else: "?name=small"
|
||||
|
|
|
@ -25,5 +25,5 @@ proc renderList*(body: VNode; query: Query; list: List): VNode =
|
|||
tdiv(class="timeline-description"):
|
||||
text list.description
|
||||
|
||||
renderListTabs(query, &"/{list.username}/lists/{list.name}")
|
||||
renderListTabs(query, &"/i/lists/{list.id}")
|
||||
body
|
||||
|
|
|
@ -42,12 +42,6 @@ proc hiddenField*(name, value: string): VNode =
|
|||
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")):
|
||||
refererField path
|
||||
button(`type`="submit"):
|
||||
icon icon, title=title
|
||||
|
||||
proc buttonReferer*(action, text, path: string; class=""; `method`="post"): VNode =
|
||||
buildHtml(form(`method`=`method`, action=action, class=class)):
|
||||
refererField path
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: "
|
||||
#end if
|
||||
#var text = stripHtml(tweet.text)
|
||||
#if unicode.runeLen(text) > 32:
|
||||
# text = unicode.runeSubStr(text, 0, 32) & "..."
|
||||
#end if
|
||||
##if unicode.runeLen(text) > 32:
|
||||
## text = unicode.runeSubStr(text, 0, 32) & "..."
|
||||
##end if
|
||||
#result &= xmltree.escape(text)
|
||||
#if result.len > 0: return
|
||||
#end if
|
||||
|
@ -111,7 +111,7 @@ ${renderRssTweets(timeline.content, cfg)}
|
|||
#
|
||||
#proc renderListRss*(tweets: seq[Tweet]; list: List; cfg: Config): string =
|
||||
#let prefs = Prefs(replaceTwitter: cfg.hostname, replaceYouTube: cfg.replaceYouTube, replaceOdysee: cfg.replaceOdysee)
|
||||
#let link = &"{getUrlPrefix(cfg)}/{list.username}/lists/{list.name}"
|
||||
#let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}"
|
||||
#result = ""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||
|
|
|
@ -7,7 +7,7 @@ import ".."/[types, utils, formatters]
|
|||
|
||||
proc getSmallPic(url: string): string =
|
||||
result = url
|
||||
if "?" notin url:
|
||||
if "?" notin url and not url.endsWith("placeholder.png"):
|
||||
result &= ":small"
|
||||
result = getPicUrl(result)
|
||||
|
||||
|
@ -66,36 +66,35 @@ proc isPlaybackEnabled(prefs: Prefs; video: Video): bool =
|
|||
of m3u8, vmap: prefs.hlsPlayback
|
||||
|
||||
proc renderVideoDisabled(video: Video; path: string): VNode =
|
||||
buildHtml(tdiv):
|
||||
img(src=getSmallPic(video.thumb))
|
||||
tdiv(class="video-overlay"):
|
||||
case video.playbackType
|
||||
of mp4:
|
||||
p: text "mp4 playback disabled in preferences"
|
||||
of m3u8, vmap:
|
||||
buttonReferer "/enablehls", "Enable hls playback", path
|
||||
buildHtml(tdiv(class="video-overlay")):
|
||||
case video.playbackType
|
||||
of mp4:
|
||||
p: text "mp4 playback disabled in preferences"
|
||||
of m3u8, vmap:
|
||||
buttonReferer "/enablehls", "Enable hls playback", path
|
||||
|
||||
proc renderVideoUnavailable(video: Video): VNode =
|
||||
buildHtml(tdiv):
|
||||
img(src=getSmallPic(video.thumb))
|
||||
tdiv(class="video-overlay"):
|
||||
case video.reason
|
||||
of "dmcaed":
|
||||
p: text "This media has been disabled in response to a report by the copyright owner"
|
||||
else:
|
||||
p: text "This media is unavailable"
|
||||
buildHtml(tdiv(class="video-overlay")):
|
||||
case video.reason
|
||||
of "dmcaed":
|
||||
p: text "This media has been disabled in response to a report by the copyright owner"
|
||||
else:
|
||||
p: text "This media is unavailable"
|
||||
|
||||
proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
||||
let container =
|
||||
if video.description.len > 0 or video.title.len > 0: " card-container"
|
||||
else: ""
|
||||
|
||||
buildHtml(tdiv(class="attachments card")):
|
||||
tdiv(class="gallery-video" & container):
|
||||
tdiv(class="attachment video-container"):
|
||||
let thumb = getSmallPic(video.thumb)
|
||||
if not video.available:
|
||||
img(src=thumb)
|
||||
renderVideoUnavailable(video)
|
||||
elif not prefs.isPlaybackEnabled(video):
|
||||
img(src=thumb)
|
||||
renderVideoDisabled(video, path)
|
||||
else:
|
||||
let vid = video.variants.filterIt(it.videoType == video.playbackType)
|
||||
|
@ -202,6 +201,8 @@ proc renderAttribution(profile: Profile): VNode =
|
|||
buildHtml(a(class="attribution", href=("/" & profile.username))):
|
||||
renderMiniAvatar(profile)
|
||||
strong: text profile.fullname
|
||||
if profile.verified:
|
||||
icon "ok", class="verified-icon", title="Verified account"
|
||||
|
||||
proc renderMediaTags(tags: seq[Profile]): VNode =
|
||||
buildHtml(tdiv(class="media-tag-block")):
|
||||
|
|
新しいイシューから参照