Add photo rail support
This commit is contained in:
parent
080d4774cf
commit
141bfdc508
8 changed files with 103 additions and 5 deletions
|
@ -27,7 +27,6 @@ is on implementing missing features.
|
|||
|
||||
- Search (images/videos, hashtags, etc.)
|
||||
- Custom timeline filter
|
||||
- Media carousel below profile
|
||||
- Media-only/gallery view
|
||||
- Nitter link previews
|
||||
- Server configuration
|
||||
|
|
|
@ -477,9 +477,13 @@ video {
|
|||
|
||||
.profile-bio {
|
||||
overflow: hidden;
|
||||
margin-right: -6px;
|
||||
overflow-wrap: break-word;
|
||||
width: 100%;
|
||||
margin: 10px -6px 0px 0px;
|
||||
}
|
||||
|
||||
.profile-bio p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-description {
|
||||
|
@ -490,6 +494,47 @@ video {
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.photo-rail-card {
|
||||
float: left;
|
||||
background: #161616;
|
||||
border-radius: 0 0 4px 4px;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.photo-rail-heading {
|
||||
padding: 5px 12px 0px 12px;
|
||||
}
|
||||
|
||||
.photo-rail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-gap: 3px 3px;
|
||||
padding: 5px 12px 12px 12px;
|
||||
}
|
||||
|
||||
.photo-rail-grid a {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.photo-rail-grid a:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.photo-rail-grid img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 380 KiB After Width: | Height: | Size: 699 KiB |
19
src/api.nim
19
src/api.nim
|
@ -15,6 +15,7 @@ const
|
|||
|
||||
timelineUrl = "i/profiles/show/$1/timeline/tweets"
|
||||
timelineSearchUrl = "i/search/timeline"
|
||||
timelineMediaUrl = "i/profiles/show/$1/media_timeline"
|
||||
profilePopupUrl = "i/profiles/popup"
|
||||
profileIntentUrl = "intent/user"
|
||||
tweetUrl = "status"
|
||||
|
@ -162,6 +163,24 @@ proc getConversationPolls*(convo: Conversation) {.async.} =
|
|||
futs.add convo.replies.map(getPolls)
|
||||
await all(futs)
|
||||
|
||||
proc getPhotoRail*(username: string): Future[seq[GalleryPhoto]] {.async.} =
|
||||
let headers = newHttpHeaders({
|
||||
"Accept": jsonAccept,
|
||||
"Referer": $(base / username),
|
||||
"User-Agent": agent,
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
})
|
||||
|
||||
let params = {
|
||||
"for_photo_rail": "true",
|
||||
"oldest_unread_id": "0"
|
||||
}
|
||||
|
||||
let url = base / (timelineMediaUrl % username) ? params
|
||||
let html = await fetchHtml(url, headers, jsonKey="items_html")
|
||||
|
||||
result = parsePhotoRail(html)
|
||||
|
||||
proc getProfileFallback(username: string; headers: HttpHeaders): Future[Profile] {.async.} =
|
||||
let url = base / profileIntentUrl ? {"screen_name": username}
|
||||
let html = await fetchHtml(url, headers)
|
||||
|
|
|
@ -12,6 +12,7 @@ proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.a
|
|||
let
|
||||
username = name.strip(chars={'/'})
|
||||
profileFut = getCachedProfile(username)
|
||||
railFut = getPhotoRail(username)
|
||||
|
||||
var timelineFut: Future[Timeline]
|
||||
if query.isNone:
|
||||
|
@ -23,7 +24,7 @@ proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.a
|
|||
if profile.username.len == 0:
|
||||
return ""
|
||||
|
||||
let profileHtml = renderProfile(profile, await timelineFut, after.len == 0)
|
||||
let profileHtml = renderProfile(profile, await timelineFut, await railFut, after.len == 0)
|
||||
return renderMain(profileHtml, title=pageTitle(profile))
|
||||
|
||||
template respTimeline(timeline: typed) =
|
||||
|
|
|
@ -163,3 +163,11 @@ proc parsePoll*(node: XmlNode): Poll =
|
|||
if n > highest:
|
||||
highest = n
|
||||
result.leader = i
|
||||
|
||||
proc parsePhotoRail*(node: XmlNode): seq[GalleryPhoto] =
|
||||
for img in node.selectAll(".tweet-media-img-placeholder"):
|
||||
result.add GalleryPhoto(
|
||||
url: img.attr("data-image-url"),
|
||||
tweetId: img.attr("data-tweet-id"),
|
||||
color: img.attr("background-color").replace("style", "background-color")
|
||||
)
|
||||
|
|
|
@ -57,6 +57,11 @@ type
|
|||
url*: string
|
||||
thumb*: string
|
||||
|
||||
GalleryPhoto* = object
|
||||
url*: string
|
||||
tweetId*: string
|
||||
color*: string
|
||||
|
||||
Poll* = object
|
||||
options*: seq[string]
|
||||
values*: seq[int]
|
||||
|
|
|
@ -41,6 +41,23 @@
|
|||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderPhotoRail(username: string; photoRail: seq[GalleryPhoto]): string =
|
||||
<div class="photo-rail-card">
|
||||
<div class="photo-rail-heading">
|
||||
<a href="/${username}/media">🖼 Photos and videos</a>
|
||||
</div>
|
||||
<div class="photo-rail-grid">
|
||||
#for i, photo in photoRail:
|
||||
#if i == 20: break
|
||||
#end if
|
||||
<a href="/${username}/status/${photo.tweetId}" style="${photo.color}">
|
||||
<img src=${getSigUrl(photo.url & ":thumb", "pic")}></img>
|
||||
</a>
|
||||
#end for
|
||||
</div>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderBanner(profile: Profile): string =
|
||||
#if "#" in profile.banner:
|
||||
<div style="${profile.banner}" class="profile-banner-color"></div>
|
||||
|
@ -90,13 +107,17 @@
|
|||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc renderProfile*(profile: Profile; timeline: Timeline; beginning: bool): string =
|
||||
#proc renderProfile*(profile: Profile; timeline: Timeline;
|
||||
# photoRail: seq[GalleryPhoto]; beginning: bool): string =
|
||||
<div class="profile-tabs">
|
||||
<div class="profile-banner">
|
||||
${renderBanner(profile)}
|
||||
</div>
|
||||
<div class="profile-tab">
|
||||
${renderProfileCard(profile)}
|
||||
#if photoRail.len > 0:
|
||||
${renderPhotoRail(profile.username, photoRail)}
|
||||
#end if
|
||||
</div>
|
||||
<div class="timeline-tab">
|
||||
#let link = "/" & profile.username
|
||||
|
|
Loading…
Reference in a new issue