mirror of
https://github.com/4cuck/teapawt.git
synced 2026-10-27 19:04:35 +00:00
ui: redesign from scratch
This commit is contained in:
1 parent
7321ee27bf
commit
1eb036d196
22 files changed
+1879
-936
No files matched your search
+2
-6
@@ -843,11 +843,7 @@ impl ApiClient {
|
||||
}
|
||||
|
||||
/// Translate a tweet using Kagi Translate API.
|
||||
pub async fn kagi_translate(
|
||||
&self,
|
||||
tweet_id: &str,
|
||||
kagi_token: &str,
|
||||
) -> Result<Translation> {
|
||||
pub async fn kagi_translate(&self, tweet_id: &str, kagi_token: &str) -> Result<Translation> {
|
||||
use http_body_util::Full;
|
||||
use hyper_rustls::HttpsConnectorBuilder;
|
||||
use hyper_util::{
|
||||
@@ -995,7 +991,7 @@ impl ApiClient {
|
||||
tweet_id: tweet.id.to_string(),
|
||||
color: String::new(),
|
||||
});
|
||||
if photos.len() >= 16 {
|
||||
if photos.len() >= 10 {
|
||||
return Ok(photos);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -155,10 +155,10 @@ async fn main() -> eyre::Result<()> {
|
||||
HeaderValue::from_static(
|
||||
"default-src 'none'; \
|
||||
script-src 'self'; \
|
||||
style-src 'self' 'unsafe-inline'; \
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
|
||||
img-src 'self' data:; \
|
||||
media-src 'self' blob:; \
|
||||
font-src 'self'; \
|
||||
font-src 'self' https://fonts.gstatic.com; \
|
||||
connect-src 'self'; \
|
||||
form-action 'self'; \
|
||||
base-uri 'self'; \
|
||||
|
||||
@@ -121,7 +121,8 @@ async fn status_media_redirect(Path((username, id)): Path<(String, String)>) ->
|
||||
|
||||
#[expect(
|
||||
clippy::cognitive_complexity,
|
||||
reason = "status handler has many conditional branches for caching, scrolling, pagination, and translation"
|
||||
reason = "status handler has many conditional branches for caching, scrolling, pagination, and \
|
||||
translation"
|
||||
)]
|
||||
async fn status(
|
||||
State(state): State<AppState>,
|
||||
@@ -534,10 +535,7 @@ async fn translate(
|
||||
(!server_token.is_empty()).then(|| server_token.clone())
|
||||
});
|
||||
|
||||
let result = state
|
||||
.api
|
||||
.translate_auto(&id, kagi_token.as_deref())
|
||||
.await;
|
||||
let result = state.api.translate_auto(&id, kagi_token.as_deref()).await;
|
||||
|
||||
match result {
|
||||
Ok(tl) if !tl.text.is_empty() => {
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ impl Prefs {
|
||||
hide_pins: false,
|
||||
hide_replies: false,
|
||||
hide_community_notes: false,
|
||||
square_avatars: false,
|
||||
square_avatars: true,
|
||||
use_twemoji: true,
|
||||
sticky_nav: true,
|
||||
proxy_media: true,
|
||||
|
||||
+29
-29
@@ -266,41 +266,41 @@ pub struct TweetStats {
|
||||
reason = "mirrors Twitter API tweet fields"
|
||||
)]
|
||||
pub struct Tweet {
|
||||
pub id: i64,
|
||||
pub thread_id: i64,
|
||||
pub reply_id: i64,
|
||||
pub user: User,
|
||||
pub text: String,
|
||||
pub id: i64,
|
||||
pub thread_id: i64,
|
||||
pub reply_id: i64,
|
||||
pub user: User,
|
||||
pub text: String,
|
||||
#[serde(with = "time::serde::timestamp::option")]
|
||||
pub time: Option<time::OffsetDateTime>,
|
||||
pub reply: Vec<String>,
|
||||
pub pinned: bool,
|
||||
pub has_thread: bool,
|
||||
pub available: bool,
|
||||
pub tombstone: String,
|
||||
pub location: String,
|
||||
pub source: String,
|
||||
pub stats: TweetStats,
|
||||
pub retweet: Option<Box<Self>>,
|
||||
pub attribution: Option<User>,
|
||||
pub media_tags: Vec<User>,
|
||||
pub quote: Option<Box<Self>>,
|
||||
pub card: Option<Card>,
|
||||
pub poll: Option<Poll>,
|
||||
pub gif: Option<Gif>,
|
||||
pub video: Option<Video>,
|
||||
pub photos: Vec<Photo>,
|
||||
pub time: Option<time::OffsetDateTime>,
|
||||
pub reply: Vec<String>,
|
||||
pub pinned: bool,
|
||||
pub has_thread: bool,
|
||||
pub available: bool,
|
||||
pub tombstone: String,
|
||||
pub location: String,
|
||||
pub source: String,
|
||||
pub stats: TweetStats,
|
||||
pub retweet: Option<Box<Self>>,
|
||||
pub attribution: Option<User>,
|
||||
pub media_tags: Vec<User>,
|
||||
pub quote: Option<Box<Self>>,
|
||||
pub card: Option<Card>,
|
||||
pub poll: Option<Poll>,
|
||||
pub gif: Option<Gif>,
|
||||
pub video: Option<Video>,
|
||||
pub photos: Vec<Photo>,
|
||||
/// Community note (Birdwatch) -- structured data, rendered in views.
|
||||
pub note: Option<CommunityNote>,
|
||||
pub note: Option<CommunityNote>,
|
||||
/// Content disclosure labels (paid promotion, AI-generated media).
|
||||
pub paid_promotion: bool,
|
||||
pub ai_generated: bool,
|
||||
pub paid_promotion: bool,
|
||||
pub ai_generated: bool,
|
||||
/// Edit history tweet IDs.
|
||||
pub history: Vec<i64>,
|
||||
pub history: Vec<i64>,
|
||||
/// Text entities for expansion (mentions, hashtags, URLs).
|
||||
pub entities: Vec<Entity>,
|
||||
pub entities: Vec<Entity>,
|
||||
/// BCP-47 language code from Twitter's language detection.
|
||||
pub lang: String,
|
||||
pub lang: String,
|
||||
/// Whether Twitter considers this tweet translatable.
|
||||
pub is_translatable: bool,
|
||||
/// Pre-fetched translation for embeds (OG tags, `ActivityPub`).
|
||||
|
||||
+16
-6
@@ -3,6 +3,7 @@ use std::sync::LazyLock;
|
||||
use maud::{
|
||||
DOCTYPE,
|
||||
Markup,
|
||||
PreEscaped,
|
||||
html,
|
||||
};
|
||||
use regex::Regex;
|
||||
@@ -10,7 +11,6 @@ use regex::Regex;
|
||||
use crate::{
|
||||
config::Config,
|
||||
types::Prefs,
|
||||
views::renderutils::icon,
|
||||
};
|
||||
|
||||
pub const STYLE_CSS: &str = "/css/style.css";
|
||||
@@ -249,13 +249,23 @@ pub fn render_navbar_full(config: &Config, rss: &str, canonical: &str, referer:
|
||||
img class="site-logo" src="/logo.svg" alt=(format!("{} logo", config.server.title));
|
||||
}
|
||||
div class="nav-item right" {
|
||||
(icon("search", "", "Search", "", "/search"))
|
||||
a class="nav-icon" title="Search" href="/search" {
|
||||
span class="material-symbols-outlined" { "search" }
|
||||
}
|
||||
@if config.config.enable_rss && !rss.is_empty() {
|
||||
(icon("rss", "", "RSS Feed", "", rss))
|
||||
a class="nav-icon" title="RSS Feed" href=(rss) {
|
||||
span class="material-symbols-outlined" { "rss_feed" }
|
||||
}
|
||||
}
|
||||
a class="nav-icon" title="Open in X" href=(&canonical) {
|
||||
(PreEscaped(r#"<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.737-8.834L1.254 2.25H8.08l4.259 5.622L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"/></svg>"#))
|
||||
}
|
||||
a class="nav-icon" title="About" href="/about" {
|
||||
span class="material-symbols-outlined" { "info" }
|
||||
}
|
||||
a class="nav-icon" title="Preferences" href=(&settings_href) {
|
||||
span class="material-symbols-outlined" { "settings" }
|
||||
}
|
||||
(icon("bird", "", "Open in X", "", &canonical))
|
||||
(icon("info", "", "About", "", "/about"))
|
||||
(icon("cog", "", "Preferences", "", &settings_href))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
-50
@@ -79,74 +79,101 @@ pub fn render_preferences_form(
|
||||
prefs_url: &str,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="overlay-panel" {
|
||||
div class="overlay-panel preferences-panel" {
|
||||
fieldset class="preferences" {
|
||||
form method="post" action="/saveprefs" autocomplete="off" {
|
||||
// Close button for animated panel
|
||||
a href=(referer) class="preferences-close" {
|
||||
span class="icon-close" {}
|
||||
}
|
||||
|
||||
// Header with animation
|
||||
div class="preferences-header" {
|
||||
h2 { "Preferences" }
|
||||
p { "Customize your teapot experience" }
|
||||
}
|
||||
|
||||
form method="post" action="/saveprefs" autocomplete="off" class="preferences-form" id="saveprefs-form" {
|
||||
input type="hidden" name="referer" value=(referer);
|
||||
|
||||
// Display section
|
||||
legend { "Display" }
|
||||
(gen_select("theme", "Theme", &prefs.theme,
|
||||
&themes.iter().map(|theme| (theme.as_str(), theme.as_str())).collect::<Vec<_>>()))
|
||||
(gen_checkbox("infiniteScroll", "Infinite scrolling (experimental, requires JavaScript)", prefs.infinite_scroll))
|
||||
(gen_checkbox("stickyProfile", "Make profile sidebar stick to top", prefs.sticky_profile))
|
||||
(gen_checkbox("stickyNav", "Keep navbar fixed to top", prefs.sticky_nav))
|
||||
(gen_checkbox("bidiSupport", "Support bidirectional text (makes clicking on tweets harder)", prefs.bidi_support))
|
||||
(gen_checkbox("hideTweetStats", "Hide tweet stats (replies, retweets, likes)", prefs.hide_tweet_stats))
|
||||
(gen_checkbox("hideBanner", "Hide profile banner", prefs.hide_banner))
|
||||
(gen_checkbox("hidePins", "Hide pinned tweets", prefs.hide_pins))
|
||||
(gen_checkbox("hideReplies", "Hide tweet replies", prefs.hide_replies))
|
||||
(gen_checkbox("hideCommunityNotes", "Hide community notes", prefs.hide_community_notes))
|
||||
(gen_checkbox("squareAvatars", "Square profile pictures", prefs.square_avatars))
|
||||
(gen_checkbox("useTwemoji", "Use Twitter emoji (Twemoji)", prefs.use_twemoji))
|
||||
div class="pref-section" data-section="display" {
|
||||
legend { "Display" }
|
||||
div class="pref-grid" {
|
||||
(gen_select("theme", "Theme", &prefs.theme, &themes.iter().map(|t| (t.as_str(), t.as_str())).collect::<Vec<_>>()))
|
||||
(gen_checkbox("infiniteScroll", "Infinite scrolling (experimental, requires JavaScript)", prefs.infinite_scroll))
|
||||
(gen_checkbox("stickyProfile", "Make profile sidebar stick to top", prefs.sticky_profile))
|
||||
(gen_checkbox("stickyNav", "Keep navbar fixed to top", prefs.sticky_nav))
|
||||
(gen_checkbox("bidiSupport", "Support bidirectional text", prefs.bidi_support))
|
||||
(gen_checkbox("hideTweetStats", "Hide tweet stats", prefs.hide_tweet_stats))
|
||||
(gen_checkbox("hideBanner", "Hide profile banner", prefs.hide_banner))
|
||||
(gen_checkbox("hidePins", "Hide pinned tweets", prefs.hide_pins))
|
||||
(gen_checkbox("hideReplies", "Hide tweet replies", prefs.hide_replies))
|
||||
(gen_checkbox("hideCommunityNotes", "Hide community notes", prefs.hide_community_notes))
|
||||
(gen_checkbox("squareAvatars", "Square profile pictures", prefs.square_avatars))
|
||||
(gen_checkbox("useTwemoji", "Use Twitter emoji (Twemoji)", prefs.use_twemoji))
|
||||
}
|
||||
}
|
||||
|
||||
// Media section
|
||||
legend { "Media" }
|
||||
(gen_checkbox("mp4Playback", "Enable mp4 video playback", prefs.mp4_playback))
|
||||
(gen_checkbox("muteVideos", "Mute videos by default", prefs.mute_videos))
|
||||
(gen_checkbox("autoplayGifs", "Autoplay gifs", prefs.autoplay_gifs))
|
||||
(gen_checkbox("proxyMedia", "Proxy media through instance (recommended for privacy)", prefs.proxy_media))
|
||||
|
||||
// Accessibility section
|
||||
legend { "Accessibility" }
|
||||
(gen_font_size_select(&prefs.font_size))
|
||||
div class="pref-section" data-section="media" {
|
||||
legend { "Media" }
|
||||
div class="pref-grid" {
|
||||
(gen_checkbox("mp4Playback", "Enable mp4 video playback", prefs.mp4_playback))
|
||||
(gen_checkbox("muteVideos", "Mute videos by default", prefs.mute_videos))
|
||||
(gen_checkbox("autoplayGifs", "Autoplay gifs", prefs.autoplay_gifs))
|
||||
}
|
||||
}
|
||||
|
||||
// Link replacements section
|
||||
legend { "Link replacements (blank to disable)" }
|
||||
(gen_input("replaceTwitter", "Twitter -> teapot", &prefs.replace_twitter, "teapot hostname"))
|
||||
(gen_input("replaceYouTube", "YouTube -> Piped/Invidious", &prefs.replace_youtube, "Piped hostname"))
|
||||
(gen_input("replaceReddit", "Reddit -> Teddit/Libreddit", &prefs.replace_reddit, "Teddit hostname"))
|
||||
div class="pref-section" data-section="links" {
|
||||
legend { "Link replacements (blank to disable)" }
|
||||
(gen_input("replaceTwitter", "Twitter -> teapot", &prefs.replace_twitter, "teapot hostname"))
|
||||
(gen_input("replaceYouTube", "YouTube -> Piped/Invidious", &prefs.replace_youtube, "Piped hostname"))
|
||||
(gen_input("replaceReddit", "Reddit -> Teddit/Libreddit", &prefs.replace_reddit, "Teddit hostname"))
|
||||
}
|
||||
|
||||
// Translation section
|
||||
legend { "Translation" }
|
||||
(gen_input("kagiToken", "Kagi session token", &prefs.kagi_token, "Paste session token here"))
|
||||
p class="bookmark-note" {
|
||||
"Uses Kagi Translate instead of Twitter. "
|
||||
a href="https://kagi.com/settings?p=user_details" target="_blank" { "Get your token here" }
|
||||
" (Session Link → copy token)"
|
||||
div class="pref-section" data-section="translation" {
|
||||
legend { "Translation" }
|
||||
(gen_input("kagiToken", "Kagi session token", &prefs.kagi_token, "Paste session token here"))
|
||||
p class="bookmark-note" {
|
||||
"Uses Kagi Translate instead of Twitter. "
|
||||
a href="https://kagi.com/settings?p=user_details" target="_blank" { "Get your token here" }
|
||||
" (Session Link → copy token)"
|
||||
}
|
||||
}
|
||||
|
||||
// Bookmark section
|
||||
legend { "Bookmark" }
|
||||
p class="bookmark-note" {
|
||||
"Save this URL to restore your preferences (?prefs works on all pages)"
|
||||
}
|
||||
pre class="prefs-code" {
|
||||
(prefs_url)
|
||||
}
|
||||
p class="bookmark-note" {
|
||||
(PreEscaped("You can override preferences with query parameters (e.g. <code>?mp4Playback=on</code>). These overrides aren't saved to cookies, and links won't retain the parameters. Intended for configuring RSS feeds and other cookieless environments. Hover over a preference to see its name."))
|
||||
div class="pref-section pref-section-bookmark" data-section="bookmark" {
|
||||
legend { "Bookmark" }
|
||||
p class="bookmark-note" {
|
||||
"Save this URL to restore your preferences (?prefs works on all pages)"
|
||||
}
|
||||
div class="prefs-code-wrapper" {
|
||||
pre class="prefs-code" {
|
||||
code { (prefs_url) }
|
||||
}
|
||||
button type="button" class="copy-prefs-btn" data-clipboard=(prefs_url) {
|
||||
span class="icon-copy" {}
|
||||
}
|
||||
}
|
||||
p class="bookmark-note" {
|
||||
(PreEscaped("You can override preferences with query parameters (e.g. <code>?mp4Playback=on</code>). These overrides aren't saved to cookies."))
|
||||
}
|
||||
}
|
||||
|
||||
h4 class="note" { "Preferences are stored client-side using cookies without any personal information." }
|
||||
|
||||
button type="submit" class="pref-submit" { "Save preferences" }
|
||||
}
|
||||
|
||||
form method="post" action="/resetprefs" class="pref-reset" {
|
||||
input type="hidden" name="referer" value=(referer);
|
||||
button type="submit" { "Reset preferences" }
|
||||
// Actions row — reset and save side by side
|
||||
div class="preferences-actions" {
|
||||
form method="post" action="/resetprefs" class="pref-reset-form" {
|
||||
input type="hidden" name="referer" value=(referer);
|
||||
button type="submit" class="pref-reset" { "Reset preferences" }
|
||||
}
|
||||
button type="submit" form="saveprefs-form" class="pref-submit" { "Save preferences" }
|
||||
}
|
||||
|
||||
h4 class="note preferences-footer-note" { "Preferences are stored client-side using cookies without any personal information." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-33
@@ -61,15 +61,18 @@ pub fn render_profile_with_prefs(
|
||||
|
||||
html! {
|
||||
div class="profile-tabs" {
|
||||
@if !hide_banner {
|
||||
div class="profile-banner" {
|
||||
(render_banner(&profile.user.banner, config))
|
||||
}
|
||||
}
|
||||
|
||||
// Left sidebar with profile card and photo rail
|
||||
div class=(profile_tab_class) {
|
||||
(render_user_card(&profile.user, config, prefs))
|
||||
// Photo rail
|
||||
div class="profile-header" {
|
||||
@if !hide_banner {
|
||||
div class="profile-banner" {
|
||||
(render_banner(&profile.user.banner, config))
|
||||
}
|
||||
}
|
||||
(render_user_card(&profile.user, config, prefs))
|
||||
}
|
||||
|
||||
// Photo rail positioned on the left side
|
||||
@if !profile.photo_rail.is_empty() {
|
||||
(render_photo_rail(&profile.photo_rail, &profile.user, config))
|
||||
}
|
||||
@@ -91,10 +94,7 @@ pub fn render_profile_with_prefs(
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
// Tabs + timeline wrapped in timeline-container
|
||||
// This is critical for the CSS flexbox layout: profile-tabs has two flex children:
|
||||
// 1. profile-tab (sidebar)
|
||||
// 2. timeline-container (main content with tabs + timeline)
|
||||
// Timeline container - main content area
|
||||
div class="timeline-container" {
|
||||
(render_timeline_tabs(timeline_kind, &profile.user.username))
|
||||
(render_timeline_with_pinned_and_prefs(groups, config, cursor, Some(&base_url), pinned, prefs, newer_url))
|
||||
@@ -126,19 +126,24 @@ pub fn render_profile_page(
|
||||
|
||||
html! {
|
||||
div class="profile-tabs" {
|
||||
@if !hide_banner {
|
||||
div class="profile-banner" {
|
||||
(render_banner(&user.banner, config))
|
||||
}
|
||||
}
|
||||
|
||||
// Left sidebar
|
||||
div class=(profile_tab_class) {
|
||||
(render_user_card(user, config, Some(prefs)))
|
||||
div class="profile-header" {
|
||||
@if !hide_banner {
|
||||
div class="profile-banner" {
|
||||
(render_banner(&user.banner, config))
|
||||
}
|
||||
}
|
||||
(render_user_card(user, config, Some(prefs)))
|
||||
}
|
||||
|
||||
// Photo rail on the left sidebar
|
||||
@if !photo_rail.is_empty() {
|
||||
(render_photo_rail(photo_rail, user, config))
|
||||
}
|
||||
}
|
||||
|
||||
// Main content area
|
||||
div class="timeline-container" {
|
||||
(render_timeline_tabs(tab, &user.username))
|
||||
(timeline_content)
|
||||
@@ -189,18 +194,17 @@ fn render_user_card(user: &User, config: &Config, prefs: Option<&Prefs>) -> Mark
|
||||
|
||||
html! {
|
||||
div class="profile-card" {
|
||||
div class="profile-card-info" {
|
||||
a class="profile-card-avatar" href=(orig_avatar_url) target="_blank" {
|
||||
(gen_img(&avatar_pic, avatar_class, config))
|
||||
}
|
||||
a class="profile-card-avatar" href=(orig_avatar_url) target="_blank" {
|
||||
(gen_img(&avatar_pic, avatar_class, config))
|
||||
}
|
||||
|
||||
div class="profile-card-tabs-name" {
|
||||
div class="profile-card-identity" {
|
||||
div {
|
||||
(link_user(user, "profile-card-fullname"))
|
||||
" "
|
||||
(verified_icon(user))
|
||||
" "
|
||||
(link_user(user, "profile-card-username"))
|
||||
}
|
||||
(link_user(user, "profile-card-username"))
|
||||
}
|
||||
|
||||
div class="profile-card-extra" {
|
||||
@@ -215,12 +219,17 @@ fn render_user_card(user: &User, config: &Config, prefs: Option<&Prefs>) -> Mark
|
||||
span { (icon("location", "", "", "", "")) }
|
||||
" "
|
||||
@let (place, url) = parse_location(&user.location);
|
||||
@let display_place = if place.chars().count() > 40 {
|
||||
format!("{}…", place.chars().take(40).collect::<String>())
|
||||
} else {
|
||||
place.clone()
|
||||
};
|
||||
@if !url.is_empty() {
|
||||
a href=(url) { (place) }
|
||||
a href=(url) title=(place) { (display_place) }
|
||||
} @else if place.contains("://") {
|
||||
a href=(place) { (place) }
|
||||
a href=(place) title=(place) { (display_place) }
|
||||
} @else {
|
||||
span { (place) }
|
||||
span title=(place) { (display_place) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,25 +296,34 @@ fn render_banner(banner: &str, config: &Config) -> Markup {
|
||||
/// Render photo rail showing recent media.
|
||||
fn render_photo_rail(photos: &[GalleryPhoto], user: &User, config: &Config) -> Markup {
|
||||
let count = formatters::format_with_commas(user.media);
|
||||
let header_text = format!("{count} Photos and videos");
|
||||
|
||||
html! {
|
||||
div class="photo-rail-card" {
|
||||
div class="photo-rail-header" {
|
||||
a href=(format!("/{}/media", user.username)) {
|
||||
(icon("picture", &header_text, "", "", ""))
|
||||
span class="photo-rail-icon-wrap" {
|
||||
span class="material-symbols-outlined photo-rail-media-icon" { "photo_library" }
|
||||
span class="photo-rail-media-badge" { (count) }
|
||||
}
|
||||
span class="photo-rail-media-label" { "Media" }
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile toggle
|
||||
input id="photo-rail-grid-toggle" type="checkbox";
|
||||
label for="photo-rail-grid-toggle" class="photo-rail-header-mobile" {
|
||||
(icon("picture", &header_text, "", "", ""))
|
||||
div class="photo-rail-header-mobile-inner" {
|
||||
span class="photo-rail-icon-wrap" {
|
||||
span class="material-symbols-outlined photo-rail-media-icon" { "photo_library" }
|
||||
span class="photo-rail-media-badge" { (count) }
|
||||
}
|
||||
span { "Media" }
|
||||
}
|
||||
(icon("down", "", "", "", ""))
|
||||
}
|
||||
|
||||
div class="photo-rail-grid" {
|
||||
@for photo in photos.iter().take(16) {
|
||||
@for photo in photos.iter().take(10) {
|
||||
@let photo_suffix = if photo.url.contains("format") || photo.url.contains("placeholder") { "" } else { ":thumb" };
|
||||
a href=(format!("/{}/status/{}#m", user.username, photo.tweet_id)) {
|
||||
(gen_img(&format!("{}{}", photo.url, photo_suffix), "", config))
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{
|
||||
|
||||
use maud::{
|
||||
Markup,
|
||||
PreEscaped,
|
||||
html,
|
||||
};
|
||||
|
||||
@@ -101,9 +102,8 @@ pub fn verified_icon(user: &User) -> Markup {
|
||||
let title = format!("Verified {lower} account");
|
||||
|
||||
html! {
|
||||
div class=(format!("verified-icon {lower}")) {
|
||||
(icon("circle", "", &title, "verified-icon-circle", ""))
|
||||
(icon("ok", "", &title, "verified-icon-check", ""))
|
||||
div class=(format!("verified-icon {lower}")) title=(title) {
|
||||
(PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="18" height="18" aria-hidden="true"><path class="verified-badge-bg" d="M 20.396 11 c -0.018 -0.646 -0.215 -1.275 -0.57 -1.816 c -0.354 -0.54 -0.852 -0.972 -1.438 -1.246 c 0.223 -0.607 0.27 -1.264 0.14 -1.897 c -0.131 -0.634 -0.437 -1.218 -0.882 -1.687 c -0.47 -0.445 -1.053 -0.75 -1.687 -0.882 c -0.633 -0.13 -1.29 -0.083 -1.897 0.14 c -0.273 -0.587 -0.704 -1.086 -1.245 -1.44 S 11.647 1.62 11 1.604 c -0.646 0.017 -1.273 0.213 -1.813 0.568 s -0.969 0.854 -1.24 1.44 c -0.608 -0.223 -1.267 -0.272 -1.902 -0.14 c -0.635 0.13 -1.22 0.436 -1.69 0.882 c -0.445 0.47 -0.749 1.055 -0.878 1.688 c -0.13 0.633 -0.08 1.29 0.144 1.896 c -0.587 0.274 -1.087 0.705 -1.443 1.245 c -0.356 0.54 -0.555 1.17 -0.574 1.817 c 0.02 0.647 0.218 1.276 0.574 1.817 c 0.356 0.54 0.856 0.972 1.443 1.245 c -0.224 0.606 -0.274 1.263 -0.144 1.896 c 0.13 0.634 0.433 1.218 0.877 1.688 c 0.47 0.443 1.054 0.747 1.687 0.878 c 0.633 0.132 1.29 0.084 1.897 -0.136 c 0.274 0.586 0.705 1.084 1.246 1.439 c 0.54 0.354 1.17 0.551 1.816 0.569 c 0.647 -0.016 1.276 -0.213 1.817 -0.567 s 0.972 -0.854 1.245 -1.44 c 0.604 0.239 1.266 0.296 1.903 0.164 c 0.636 -0.132 1.22 -0.447 1.68 -0.907 c 0.46 -0.46 0.776 -1.044 0.908 -1.681 s 0.075 -1.299 -0.165 -1.903 c 0.586 -0.274 1.084 -0.705 1.439 -1.246 c 0.354 -0.54 0.551 -1.17 0.569 -1.816 Z"/><path class="verified-badge-check" d="M 9.662 14.85 l -3.429 -3.428 l 1.293 -1.302 l 2.072 2.072 l 4.4 -4.794 l 1.347 1.246 Z"/></svg>"#))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user