Add photo rail support
This commit is contained in:
parent
080d4774cf
commit
141bfdc508
|
@ -27,7 +27,6 @@ is on implementing missing features.
|
||||||
|
|
||||||
- Search (images/videos, hashtags, etc.)
|
- Search (images/videos, hashtags, etc.)
|
||||||
- Custom timeline filter
|
- Custom timeline filter
|
||||||
- Media carousel below profile
|
|
||||||
- Media-only/gallery view
|
- Media-only/gallery view
|
||||||
- Nitter link previews
|
- Nitter link previews
|
||||||
- Server configuration
|
- Server configuration
|
||||||
|
|
|
@ -477,9 +477,13 @@ video {
|
||||||
|
|
||||||
.profile-bio {
|
.profile-bio {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-right: -6px;
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 10px -6px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-bio p {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-description {
|
.profile-description {
|
||||||
|
@ -490,6 +494,47 @@ video {
|
||||||
word-wrap: break-word;
|
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 {
|
.tab {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
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"
|
timelineUrl = "i/profiles/show/$1/timeline/tweets"
|
||||||
timelineSearchUrl = "i/search/timeline"
|
timelineSearchUrl = "i/search/timeline"
|
||||||
|
timelineMediaUrl = "i/profiles/show/$1/media_timeline"
|
||||||
profilePopupUrl = "i/profiles/popup"
|
profilePopupUrl = "i/profiles/popup"
|
||||||
profileIntentUrl = "intent/user"
|
profileIntentUrl = "intent/user"
|
||||||
tweetUrl = "status"
|
tweetUrl = "status"
|
||||||
|
@ -162,6 +163,24 @@ proc getConversationPolls*(convo: Conversation) {.async.} =
|
||||||
futs.add convo.replies.map(getPolls)
|
futs.add convo.replies.map(getPolls)
|
||||||
await all(futs)
|
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.} =
|
proc getProfileFallback(username: string; headers: HttpHeaders): Future[Profile] {.async.} =
|
||||||
let url = base / profileIntentUrl ? {"screen_name": username}
|
let url = base / profileIntentUrl ? {"screen_name": username}
|
||||||
let html = await fetchHtml(url, headers)
|
let html = await fetchHtml(url, headers)
|
||||||
|
|
|
@ -12,10 +12,11 @@ proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.a
|
||||||
let
|
let
|
||||||
username = name.strip(chars={'/'})
|
username = name.strip(chars={'/'})
|
||||||
profileFut = getCachedProfile(username)
|
profileFut = getCachedProfile(username)
|
||||||
|
railFut = getPhotoRail(username)
|
||||||
|
|
||||||
var timelineFut: Future[Timeline]
|
var timelineFut: Future[Timeline]
|
||||||
if query.isNone:
|
if query.isNone:
|
||||||
timelineFut = getTimeline(username, after)
|
timelineFut = getTimeline(username, after)
|
||||||
else:
|
else:
|
||||||
timelineFut = getTimelineSearch(username, after, get(query))
|
timelineFut = getTimelineSearch(username, after, get(query))
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.a
|
||||||
if profile.username.len == 0:
|
if profile.username.len == 0:
|
||||||
return ""
|
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))
|
return renderMain(profileHtml, title=pageTitle(profile))
|
||||||
|
|
||||||
template respTimeline(timeline: typed) =
|
template respTimeline(timeline: typed) =
|
||||||
|
|
|
@ -163,3 +163,11 @@ proc parsePoll*(node: XmlNode): Poll =
|
||||||
if n > highest:
|
if n > highest:
|
||||||
highest = n
|
highest = n
|
||||||
result.leader = i
|
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
|
url*: string
|
||||||
thumb*: string
|
thumb*: string
|
||||||
|
|
||||||
|
GalleryPhoto* = object
|
||||||
|
url*: string
|
||||||
|
tweetId*: string
|
||||||
|
color*: string
|
||||||
|
|
||||||
Poll* = object
|
Poll* = object
|
||||||
options*: seq[string]
|
options*: seq[string]
|
||||||
values*: seq[int]
|
values*: seq[int]
|
||||||
|
|
|
@ -41,6 +41,23 @@
|
||||||
</div>
|
</div>
|
||||||
#end proc
|
#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 =
|
#proc renderBanner(profile: Profile): string =
|
||||||
#if "#" in profile.banner:
|
#if "#" in profile.banner:
|
||||||
<div style="${profile.banner}" class="profile-banner-color"></div>
|
<div style="${profile.banner}" class="profile-banner-color"></div>
|
||||||
|
@ -90,13 +107,17 @@
|
||||||
</div>
|
</div>
|
||||||
#end proc
|
#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-tabs">
|
||||||
<div class="profile-banner">
|
<div class="profile-banner">
|
||||||
${renderBanner(profile)}
|
${renderBanner(profile)}
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-tab">
|
<div class="profile-tab">
|
||||||
${renderProfileCard(profile)}
|
${renderProfileCard(profile)}
|
||||||
|
#if photoRail.len > 0:
|
||||||
|
${renderPhotoRail(profile.username, photoRail)}
|
||||||
|
#end if
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-tab">
|
<div class="timeline-tab">
|
||||||
#let link = "/" & profile.username
|
#let link = "/" & profile.username
|
||||||
|
|
Loading…
Reference in New Issue