Nix build doesn't work, blocked on https://github.com/kolloch/crate2nix/issues/207
This commit is contained in:
Michael Zhang 2021-08-03 03:47:17 -05:00
commit ca1cdc8179
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
43 changed files with 5126 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2495
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

17
Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "shitter"
version = "0.1.0"
edition = "2018"
[dependencies]
clap = "3.0.0-beta.2"
parking_lot = "0.11.1"
reqwest = { version = "0.11.4", default-features = false, features = ["gzip", "json", "rustls-tls"] }
rocket = "0.5.0-rc.1"
serde = { version = "1.0.126", features = ["derive"] }
serde_json = "1.0.64"
stderrlog = "0.5.1"
thiserror = "1.0.26"
tokio = { version = "1.9.0", features = ["full"] }
typed-html = { git = "https://github.com/bodil/typed-html", rev = "d95ce1a2930f2385d4f3765d061dbeff3503107f" }
url = "2.2.2"

4
README.md Normal file
View file

@ -0,0 +1,4 @@
shitter: Twitter Client
===
License: AGPL

12
default.nix Normal file
View file

@ -0,0 +1,12 @@
(import
(let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{
src = ./.;
}).defaultNix

125
flake.lock Normal file
View file

@ -0,0 +1,125 @@
{
"nodes": {
"crate2nix": {
"flake": false,
"locked": {
"lastModified": 1627923438,
"narHash": "sha256-/O/FDJynVXOdFhSXQGU2uH/FQF3MS93WMyeY+9fcRaU=",
"owner": "kolloch",
"repo": "crate2nix",
"rev": "29460b5c411defa5e8e0851fe7ecbca0f0fa41d3",
"type": "github"
},
"original": {
"owner": "kolloch",
"repo": "crate2nix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1627913399,
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1614513358,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1627978428,
"narHash": "sha256-813SF+K9wEwHVTOhgVpaC3CSZWcjap7Pv3bWppV0U44=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3ca5f904aa0f3341413f14e3bd8303a6acd39de",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1617325113,
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crate2nix": "crate2nix",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"utils": "utils"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1627957145,
"narHash": "sha256-cY5lS2S/RMsC1xFtkcmhLXlVP7ahZoxFeKedkXDvIzY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "ab6f3086de97980e4fdcb0560921852a407e0b79",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1623875721,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

43
flake.nix Normal file
View file

@ -0,0 +1,43 @@
{
description = "shitter";
inputs = {
utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
crate2nix = { url = "github:kolloch/crate2nix"; flake = false; };
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
};
outputs = { self, nixpkgs, utils, rust-overlay, crate2nix, ... }:
let
name = "shitter";
rustChannel = "stable";
in
utils.lib.eachDefaultSystem
(system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlay
(self: super: {
rustc = self.rust-bin.${rustChannel}.latest.default;
cargo = self.rust-bin.${rustChannel}.latest.default;
})
];
};
inherit (import "${crate2nix}/tools.nix" { inherit pkgs; }) generatedCargoNix;
project = pkgs.callPackage
(generatedCargoNix { inherit name; src = ./.; })
{ defaultCrateOverrides = pkgs.defaultCrateOverrides; };
buildInputs = with pkgs; [];
in rec {
packages.${name} = project.rootCrate.build;
defaultPackage = packages.${name};
apps.${name} = utils.lib.mkApp {
inherit name;
drv = packages.${name};
};
defaultApp = apps.${name};
}
);
}

39
sass/general.scss Normal file
View file

@ -0,0 +1,39 @@
@import '_variables';
@import '_mixins';
.panel-container {
margin: auto;
font-size: 130%;
}
.error-panel {
@include center-panel(var(--error_red));
text-align: center;
}
.search-bar > form {
@include center-panel(var(--darkest_grey));
button {
background: var(--bg_elements);
color: var(--fg_color);
border: 0;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
width: 30px;
height: 30px;
}
input {
font-size: 16px;
width: 100%;
background: var(--bg_elements);
color: var(--fg_color);
border: 0;
border-radius: 4px;
padding: 4px;
margin-right: 8px;
height: unset;
}
}

97
sass/include/_mixins.css Normal file
View file

@ -0,0 +1,97 @@
@import '_variables';
@mixin panel($width, $max-width) {
max-width: $max-width;
margin: 0 auto;
float: none;
border-radius: 0;
position: relative;
width: $width;
}
@mixin play-button {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
&:hover {
.overlay-circle {
border-color: var(--play_button_hover);
}
.overlay-triangle {
border-color: transparent transparent transparent var(--play_button_hover);
}
}
}
@mixin breakable {
overflow: hidden;
overflow-wrap: break-word;
}
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin center-panel($bg) {
padding: 12px;
border-radius: 4px;
display: flex;
background: $bg;
box-shadow: 0 0 15px $shadow_dark;
margin: auto;
margin-top: -50px;
}
@mixin input-colors {
&:hover {
border-color: var(--accent);
}
&:active {
border-color: var(--accent_light);
}
}
@mixin search-resize($width, $rows) {
@media(max-width: $width) {
.search-toggles {
grid-template-columns: repeat($rows, auto);
}
#search-panel-toggle:checked ~ .search-panel {
@if $rows == 6 {
max-height: 200px !important;
}
@if $rows == 5 {
max-height: 300px !important;
}
@if $rows == 4 {
max-height: 300px !important;
}
@if $rows == 3 {
max-height: 365px !important;
}
}
}
}
@mixin create-toggle($elem, $height) {
##{$elem}-toggle {
display: none;
&:checked ~ .#{$elem} {
max-height: $height;
}
&:checked ~ label .icon-down:before {
transform: rotate(180deg) translateY(-1px);
}
}
}

View file

@ -0,0 +1,44 @@
// colors
$bg_color: #0F0F0F;
$fg_color: #F8F8F2;
$fg_faded: #F8F8F2CF;
$fg_dark: #FF6C60;
$fg_nav: #FF6C60;
$bg_panel: #161616;
$bg_elements: #121212;
$bg_overlays: #1F1F1F;
$bg_hover: #1A1A1A;
$grey: #888889;
$dark_grey: #404040;
$darker_grey: #282828;
$darkest_grey: #222222;
$border_grey: #3E3E35;
$accent: #FF6C60;
$accent_light: #FFACA0;
$accent_dark: #8A3731;
$accent_border: #FF6C6091;
$play_button: #D8574D;
$play_button_hover: #FF6C60;
$more_replies_dots: #AD433B;
$error_red: #420A05;
$verified_blue: #1DA1F2;
$icon_text: $fg_color;
$tab: $fg_color;
$tab_selected: $accent;
$shadow: rgba(0,0,0,.6);
$shadow_dark: rgba(0,0,0,.2);
//fonts
$font_0: Helvetica Neue;
$font_1: Helvetica;
$font_2: Arial;
$font_3: sans-serif;
$font_4: fontello;

164
sass/index.scss Normal file
View file

@ -0,0 +1,164 @@
@import '_variables';
@import 'tweet/_base';
@import 'profile/_base';
@import 'general';
@import 'navbar';
@import 'inputs';
@import 'timeline';
@import 'search';
body {
// colors
--bg_color: #{$bg_color};
--fg_color: #{$fg_color};
--fg_faded: #{$fg_faded};
--fg_dark: #{$fg_dark};
--fg_nav: #{$fg_nav};
--bg_panel: #{$bg_panel};
--bg_elements: #{$bg_elements};
--bg_overlays: #{$bg_overlays};
--bg_hover: #{$bg_hover};
--grey: #{$grey};
--dark_grey: #{$dark_grey};
--darker_grey: #{$darker_grey};
--darkest_grey: #{$darkest_grey};
--border_grey: #{$border_grey};
--accent: #{$accent};
--accent_light: #{$accent_light};
--accent_dark: #{$accent_dark};
--accent_border: #{$accent_border};
--play_button: #{$play_button};
--play_button_hover: #{$play_button_hover};
--more_replies_dots: #{$more_replies_dots};
--error_red: #{$error_red};
--verified_blue: #{$verified_blue};
--icon_text: #{$icon_text};
--tab: #{$fg_color};
--tab_selected: #{$accent};
--profile_stat: #{$fg_color};
background-color: var(--bg_color);
color: var(--fg_color);
font-family: $font_0, $font_1, $font_2, $font_3;
font-size: 14px;
line-height: 1.3;
margin: 0;
}
* {
outline: unset;
margin: 0;
text-decoration: none;
}
h1 {
display: inline;
}
h2, h3 {
font-weight: normal;
}
p {
margin: 14px 0;
}
a {
color: var(--accent);
&:hover {
text-decoration: underline;
}
}
fieldset {
border: 0;
padding: 0;
margin-top: -0.6em;
}
legend {
width: 100%;
padding: .6em 0 .3em 0;
border: 0;
font-size: 16px;
border-bottom: 1px solid var(--border_grey);
margin-bottom: 8px;
}
.cookie-note {
border-top: 1px solid var(--border_grey);
border-bottom: 1px solid var(--border_grey);
padding: 6px 0 8px 0;
margin-bottom: 8px;
margin-top: 16px;
}
ul {
padding-left: 1.3em;
}
.container {
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
padding-top: 50px;
margin: auto;
min-height: 100vh;
}
.icon-container {
display: inline;
}
.overlay-panel {
max-width: 600px;
width: 100%;
margin: 0 auto;
margin-top: 10px;
background-color: var(--bg_overlays);
padding: 10px 15px;
align-self: start;
ul {
margin-bottom: 14px;
}
p {
word-break: break-word;
}
}
.verified-icon {
color: var(--icon_text);
background-color: var(--verified_blue);
border-radius: 50%;
flex-shrink: 0;
margin: 2px 0 3px 3px;
padding-top: 2px;
height: 12px;
width: 14px;
font-size: 8px;
display: inline-block;
text-align: center;
vertical-align: middle;
}
@media(max-width: 600px) {
.preferences-container {
max-width: 95vw;
}
.nav-item, .nav-item .icon-container {
font-size: 16px;
}
}

185
sass/inputs.scss Normal file
View file

@ -0,0 +1,185 @@
@import '_variables';
@import '_mixins';
button {
@include input-colors;
background-color: var(--bg_elements);
color: var(--fg_color);
border: 1px solid var(--accent_border);
padding: 3px 6px;
font-size: 14px;
cursor: pointer;
float: right;
}
input[type="text"],
input[type="date"],
select {
@include input-colors;
background-color: var(--bg_elements);
padding: 1px 4px;
color: var(--fg_color);
border: 1px solid var(--accent_border);
border-radius: 0;
font-size: 14px;
}
input[type="text"] {
height: 16px;
}
select {
height: 20px;
padding: 0 2px;
line-height: 1;
}
input[type="date"]::-webkit-inner-spin-button {
display: none;
}
input[type="date"]::-webkit-clear-button {
margin-left: 17px;
filter: grayscale(100%);
filter: hue-rotate(120deg);
}
input::-webkit-calendar-picker-indicator {
opacity: 0;
}
input::-webkit-datetime-edit-day-field:focus,
input::-webkit-datetime-edit-month-field:focus,
input::-webkit-datetime-edit-year-field:focus {
background-color: var(--accent);
color: var(--fg_color);
outline: none;
}
.date-range {
.date-input {
display: inline-block;
position: relative;
}
.icon-container {
pointer-events: none;
position: absolute;
top: 2px;
right: 5px;
}
.search-title {
margin: 0 2px;
}
}
.icon-button button {
color: var(--accent);
text-decoration: none;
background: none;
border: none;
float: none;
padding: unset;
padding-left: 4px;
&:hover {
color: var(--accent_light);
}
}
.checkbox {
position: absolute;
top: 1px;
right: 0;
height: 17px;
width: 17px;
background-color: var(--bg_elements);
border: 1px solid var(--accent_border);
&:after {
content: "";
position: absolute;
display: none;
}
}
.checkbox-container {
display: block;
position: relative;
margin-bottom: 5px;
cursor: pointer;
user-select: none;
padding-right: 22px;
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .checkbox:after {
display: block;
}
}
&:hover input ~ .checkbox {
border-color: var(--accent);
}
&:active input ~ .checkbox {
border-color: var(--accent_light);
}
.checkbox:after {
left: 2px;
bottom: 0;
font-size: 13px;
font-family: $font_4;
content: '\e803';
}
}
.pref-group {
display: inline;
}
.preferences {
button {
margin: 6px 0 3px 0;
}
label {
padding-right: 150px;
}
select {
position: absolute;
top: 0;
right: 0;
display: block;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
}
input[type="text"] {
position: absolute;
right: 0;
max-width: 140px;
}
.pref-group {
display: block;
}
.pref-input {
position: relative;
margin-bottom: 6px;
}
.pref-reset {
float: left;
}
}

88
sass/navbar.scss Normal file
View file

@ -0,0 +1,88 @@
@import '_variables';
nav {
display: flex;
align-items: center;
position: fixed;
background-color: var(--bg_overlays);
box-shadow: 0 0 4px $shadow;
padding: 0;
width: 100%;
height: 50px;
z-index: 1000;
font-size: 16px;
a, .icon-button button {
color: var(--fg_nav);
}
}
.inner-nav {
margin: auto;
box-sizing: border-box;
padding: 0 10px;
display: flex;
align-items: center;
flex-basis: 920px;
height: 50px;
}
.site-name {
font-size: 15px;
font-weight: 600;
line-height: 1;
&:hover {
color: var(--accent_light);
text-decoration: unset;
}
}
.site-logo {
display: block;
width: 35px;
height: 35px;
}
.nav-item {
display: flex;
flex: 1;
line-height: 50px;
height: 50px;
overflow: hidden;
flex-wrap: wrap;
align-items: center;
&.right {
text-align: right;
justify-content: flex-end;
}
&.right a {
padding-left: 4px;
&:hover {
color: var(--accent_light);
text-decoration: unset;
}
}
}
.lp {
height: 14px;
margin-top: 2px;
display: block;
fill: var(--fg_nav);
&:hover {
fill: var(--accent_light);
}
}
.icon-info:before {
margin: 0 -3px;
}
.icon-cog {
font-size: 15px;
}

73
sass/profile/_base.scss Normal file
View file

@ -0,0 +1,73 @@
@import '_variables';
@import '_mixins';
@import 'card';
@import 'photo-rail';
.profile-tabs {
@include panel(auto, 900px);
.timeline-container {
float: right;
width: 68% !important;
max-width: unset;
}
}
.profile-banner {
padding-bottom: 4px;
a {
display: inherit;
line-height: 0;
}
img {
width: 100%;
}
}
.profile-banner-color {
width: 100%;
padding-bottom: 25%;
}
.profile-tab {
padding: 0 4px 0 0;
box-sizing: border-box;
display: inline-block;
font-size: 14px;
text-align: left;
vertical-align: top;
max-width: 32%;
top: 50px;
}
.profile-result .username {
margin: 0 !important;
}
.profile-result .tweet-header {
margin-bottom: unset;
}
@media(max-width: 600px) {
.profile-tabs {
width: 100vw;
.timeline-container {
width: 100% !important;
.tab-item wide {
flex-grow: 1.4;
}
}
}
.profile-tab {
width: 100%;
max-width: unset;
position: initial !important;
padding: 0;
}
}

124
sass/profile/card.scss Normal file
View file

@ -0,0 +1,124 @@
@import '_variables';
@import '_mixins';
.profile-card {
flex-wrap: wrap;
background: var(--bg_panel);
padding: 12px;
display: flex;
}
.profile-card-info {
@include breakable;
width: 100%;
}
.profile-card-tabs-name {
@include breakable;
max-width: 100%;
}
.profile-card-username {
@include breakable;
color: var(--fg_color);
font-size: 14px;
display: block;
}
.profile-card-fullname {
@include breakable;
color: var(--fg_color);
font-size: 16px;
font-weight: bold;
text-shadow: none;
max-width: 100%;
}
.profile-card-avatar {
display: block;
width: 100%;
padding-bottom: 6px;
margin-right: 4px;
img {
display: block;
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
border: 4px solid var(--darker_grey);
background: var(--bg_color);
}
}
.profile-card-extra {
display: contents;
flex: 100%;
margin-top: 7px;
.profile-bio {
@include breakable;
width: 100%;
margin: 4px -6px 6px 0;
white-space: pre-wrap;
p {
margin: 0;
}
}
.profile-joindate, .profile-location, profile-website {
color: var(--fg_faded);
margin: 2px 0;
width: 100%;
}
}
.profile-card-extra-links {
margin-top: 8px;
font-size: 14px;
width: 100%;
}
.profile-statlist {
display: flex;
flex-wrap: wrap;
padding: 0;
width: 100%;
justify-content: space-between;
li {
display: table-cell;
text-align: center;
}
}
.profile-stat-header {
font-weight: bold;
color: var(--profile_stat);
}
.profile-stat-num {
display: block;
color: var(--profile_stat);
}
@media(max-width: 600px) {
.profile-card-info {
display: flex;
}
.profile-card-tabs-name {
@include breakable;
}
.profile-card-avatar {
height: 60px;
width: unset;
img {
border-width: 2px;
width: unset;
}
}
}

View file

@ -0,0 +1,97 @@
@import '_variables';
.photo-rail {
&-card {
float: left;
background: var(--bg_panel);
border-radius: 0 0 4px 4px;
width: 100%;
margin: 5px 0;
}
&-header {
padding: 5px 12px 0;
}
&-header-mobile {
display: none;
box-sizing: border-box;
padding: 5px 12px 0;
width: 100%;
float: unset;
color: var(--accent);
justify-content: space-between;
}
&-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 3px 3px;
padding: 5px 12px 12px;
a {
position: relative;
border-radius: 5px;
&:before {
content: "";
display: block;
padding-top: 100%;
}
}
img {
height: 100%;
width: 100%;
border-radius: 4px;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
}
}
@include create-toggle(photo-rail-grid, 640px);
#photo-rail-grid-toggle:checked ~ .photo-rail-grid {
padding-bottom: 12px;
}
@media(max-width: 600px) {
.photo-rail-header {
display: none;
}
.photo-rail-header-mobile {
display: flex;
}
.photo-rail-grid {
max-height: 0;
padding-bottom: 0;
overflow: hidden;
transition: max-height 0.4s;
}
}
@media(max-width: 600px) {
.photo-rail-grid {
grid-template-columns: repeat(6, 1fr);
}
#photo-rail-grid-toggle:checked ~ .photo-rail-grid {
max-height: 300px;
}
}
@media(max-width: 450px) {
.photo-rail-grid {
grid-template-columns: repeat(4, 1fr);
}
#photo-rail-grid-toggle:checked ~ .photo-rail-grid {
max-height: 450px;
}
}

120
sass/search.scss Normal file
View file

@ -0,0 +1,120 @@
@import '_variables';
@import '_mixins';
.search-title {
font-weight: bold;
display: inline-block;
margin-top: 4px;
}
.search-field {
display: flex;
flex-wrap: wrap;
button {
margin: 0 2px 0 0;
height: 23px;
}
.pref-input {
margin: 0 4px 0 0;
flex-grow: 1;
height: 23px;
}
input[type="text"] {
height: calc(100% - 4px);
width: calc(100% - 8px);
}
> label {
display: inline;
background-color: var(--bg_elements);
color: var(--fg_color);
border: 1px solid var(--accent_border);
padding: 1px 6px 2px 6px;
font-size: 14px;
cursor: pointer;
margin-bottom: 2px;
@include input-colors;
}
@include create-toggle(search-panel, 200px);
}
.search-panel {
width: 100%;
max-height: 0;
overflow: hidden;
transition: max-height 0.4s;
flex-grow: 1;
font-weight: initial;
text-align: left;
> div {
line-height: 1.7em;
}
.checkbox-container {
display: inline;
padding-right: unset;
margin-bottom: unset;
margin-left: 23px;
}
.checkbox {
right: unset;
left: -22px;
}
.checkbox-container .checkbox:after {
top: -4px;
}
}
.search-row {
display: flex;
flex-wrap: wrap;
line-height: unset;
> div {
flex-grow: 1;
flex-shrink: 1;
}
input {
height: 21px;
}
.pref-input {
display: block;
padding-bottom: 5px;
input {
height: 21px;
margin-top: 1px;
}
}
}
.search-toggles {
flex-grow: 1;
display: grid;
grid-template-columns: repeat(6, auto);
grid-column-gap: 10px;
}
.profile-tabs {
@include search-resize(820px, 5);
@include search-resize(725px, 4);
@include search-resize(600px, 6);
@include search-resize(560px, 5);
@include search-resize(480px, 4);
@include search-resize(410px, 3);
}
@include search-resize(560px, 5);
@include search-resize(480px, 4);
@include search-resize(410px, 3);

161
sass/timeline.scss Normal file
View file

@ -0,0 +1,161 @@
@import '_variables';
.timeline-container {
@include panel(100%, 600px);
}
.timeline {
background-color: var(--bg_panel);
> div:not(:first-child) {
border-top: 1px solid var(--border_grey);
}
}
.timeline-header {
width: 100%;
background-color: var(--bg_panel);
text-align: center;
padding: 8px;
display: block;
font-weight: bold;
margin-bottom: 5px;
box-sizing: border-box;
button {
float: unset;
}
}
.timeline-banner img {
width: 100%;
}
.timeline-description {
font-weight: normal;
}
.tab {
align-items: center;
display: flex;
flex-wrap: wrap;
list-style: none;
margin: 0 0 5px 0;
background-color: var(--bg_panel);
padding: 0;
}
.tab-item {
flex: 1 1 0;
text-align: center;
margin-top: 0;
a {
border-bottom: .1rem solid transparent;
color: var(--tab);
display: block;
padding: 8px 0;
text-decoration: none;
font-weight: bold;
&:hover {
text-decoration: none;
}
&.active {
border-bottom-color: var(--tab_selected);
color: var(--tab_selected);
}
}
&.active a {
border-bottom-color: var(--tab_selected);
color: var(--tab_selected);
}
&.wide {
flex-grow: 1.2;
}
}
.timeline-footer {
background-color: var(--bg_panel);
padding: 6px 0;
}
.timeline-protected {
text-align: center;
p {
margin: 8px 0;
}
h2 {
color: var(--accent);
font-size: 20px;
font-weight: 600;
}
}
.timeline-none {
color: var(--accent);
font-size: 20px;
font-weight: 600;
text-align: center;
}
.timeline-end {
background-color: var(--bg_panel);
color: var(--accent);
font-size: 16px;
font-weight: 600;
text-align: center;
}
.show-more {
background-color: var(--bg_panel);
text-align: center;
padding: .75em 0;
display: block !important;
a {
background-color: var(--darkest_grey);
display: inline-block;
height: 2em;
padding: 0 2em;
line-height: 2em;
&:hover {
background-color: var(--darker_grey);
}
}
}
.top-ref {
background-color: var(--bg_color);
border-top: none !important;
.icon-down {
font-size: 20px;
display: flex;
justify-content: center;
text-decoration: none;
&:hover {
color: var(--accent_light);
}
&::before {
transform: rotate(180deg) translateY(-1px);
}
}
}
.timeline-item {
overflow-wrap: break-word;
border-left-width: 0;
min-width: 0;
padding: .75em;
display: flex;
position: relative;
}

205
sass/tweet/_base.scss Normal file
View file

@ -0,0 +1,205 @@
@import '_variables';
@import '_mixins';
@import 'thread';
@import 'media';
@import 'video';
@import 'embed';
@import 'card';
@import 'poll';
@import 'quote';
.tweet-body {
flex: 1;
min-width: 0;
margin-left: 58px;
pointer-events: none;
z-index: 1;
}
.tweet-content {
font-family: $font_3;
line-height: 1.3em;
pointer-events: all;
display: inline;
}
.tweet-bidi {
display: block !important;
}
.tweet-header {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
margin-bottom: .2em;
a {
display: inline-block;
word-break: break-all;
max-width: 100%;
pointer-events: all;
}
}
.tweet-name-row {
padding: 0;
display: flex;
justify-content: space-between;
}
.fullname-and-username {
display: flex;
min-width: 0;
}
.fullname {
@include ellipsis;
flex-shrink: 2;
max-width: 80%;
font-size: 14px;
font-weight: 700;
color: var(--fg_color);
}
.username {
@include ellipsis;
min-width: 1.6em;
margin-left: .4em;
word-wrap: normal;
}
.tweet-date {
display: flex;
flex-shrink: 0;
margin-left: 4px;
}
.tweet-date a, .username, .show-more a {
color: var(--fg_dark);
}
.tweet-published {
margin: 0;
margin-top: 5px;
color: var(--grey);
pointer-events: all;
}
.tweet-avatar {
display: contents !important;
img {
float: left;
margin-top: 3px;
margin-left: -58px;
width: 48px;
height: 48px;
}
}
.avatar {
border-radius: 50%;
}
.avatar.mini {
margin-right: 5px;
margin-top: -1px;
width: 20px;
height: 20px;
}
.attribution {
display: flex;
pointer-events: all;
margin: 5px 0;
strong {
color: var(--fg_color);
}
}
.media-tag-block {
padding-top: 5px;
pointer-events: all;
color: var(--fg_faded);
.icon-container {
padding-right: 2px;
}
.media-tag, .icon-container {
color: var(--fg_faded);
}
}
.timeline-container .media-tag-block {
font-size: 13px;
}
.tweet-geo {
color: var(--fg_faded);
}
.replying-to {
color: var(--fg_faded);
margin: -2px 0 4px;
a {
pointer-events: all;
}
}
.retweet-header, .pinned, .tweet-stats {
align-content: center;
color: var(--grey);
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
font-size: 14px;
font-weight: 600;
line-height: 22px;
span {
@include ellipsis;
}
}
.retweet-header {
margin-top: -5px !important;
}
.tweet-stats {
margin-bottom: -3px;
}
.tweet-stat {
padding-top: 5px;
padding-right: 8px;
}
.show-thread {
display: block;
pointer-events: all;
padding-top: 2px;
}
.unavailable-box {
width: 100%;
height: 100%;
padding: 12px;
border: solid 1px var(--dark_grey);
border-radius: 10px;
background-color: var(--bg_color);
}
.tweet-link {
height: 100%;
width: 100%;
left: 0;
top: 0;
position: absolute;
&:hover {
background-color: var(--bg_hover);
}
}

118
sass/tweet/card.scss Normal file
View file

@ -0,0 +1,118 @@
@import '_variables';
@import '_mixins';
.card {
margin: 5px 0;
pointer-events: all;
max-height: unset;
}
.card-container {
border-radius: 10px;
border-width: 1px;
border-style: solid;
border-color: var(--dark_grey);
background-color: var(--bg_elements);
overflow: hidden;
color: inherit;
display: flex;
flex-direction: row;
text-decoration: none !important;
&:hover {
border-color: var(--grey);
}
.attachments {
margin: 0;
border-radius: 0;
}
}
.card-content {
padding: 0.5em;
}
.card-title {
@include ellipsis;
white-space: unset;
font-weight: bold;
font-size: 1.15em;
}
.card-description {
margin: 0.3em 0;
}
.card-destination {
@include ellipsis;
color: var(--grey);
display: block;
}
.card-content-container {
color: unset;
overflow: auto;
&:hover {
text-decoration: none;
}
}
.card-image-container {
width: 98px;
flex-shrink: 0;
position: relative;
overflow: hidden;
&:before {
content: "";
display: block;
padding-top: 100%;
}
}
.card-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: var(--bg_overlays);
img {
width: 100%;
height: 100%;
max-height: 400px;
display: block;
object-fit: cover;
}
}
.card-overlay {
@include play-button;
opacity: 0.8;
display: flex;
justify-content: center;
align-items: center;
}
.large {
.card-container {
display: block;
}
.card-image-container {
width: unset;
&:before {
display: none;
}
}
.card-image {
position: unset;
border-style: solid;
border-color: var(--dark_grey);
border-width: 0;
border-bottom-width: 1px;
}
}

17
sass/tweet/embed.scss Normal file
View file

@ -0,0 +1,17 @@
@import '_variables';
@import '_mixins';
.embed-video {
.gallery-video {
width: 100%;
height: 100%;
position: absolute;
background-color: black;
top: 0%;
left: 0%;
}
.video-container {
max-height: unset;
}
}

119
sass/tweet/media.scss Normal file
View file

@ -0,0 +1,119 @@
@import '_variables';
.gallery-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
overflow: hidden;
flex-grow: 1;
max-height: 379.5px;
max-width: 533px;
pointer-events: all;
.still-image {
width: 100%;
display: flex;
}
}
.attachments {
margin-top: .35em;
display: flex;
flex-direction: row;
width: 100%;
max-height: 600px;
border-radius: 7px;
overflow: hidden;
flex-flow: column;
background-color: var(--bg_color);
align-items: center;
pointer-events: all;
.image-attachment {
width: 100%;
}
}
.attachment {
position: relative;
line-height: 0;
overflow: hidden;
margin: 0 .25em 0 0;
flex-grow: 1;
box-sizing: border-box;
min-width: 2em;
&:last-child {
margin: 0;
max-height: 530px;
}
}
.gallery-gif video {
max-height: 530px;
background-color: #101010;
}
.still-image {
max-height: 379.5px;
max-width: 533px;
justify-content: center;
img {
object-fit: cover;
max-width: 100%;
max-height: 379.5px;
flex-basis: 300px;
flex-grow: 1;
}
}
.image {
display: inline-block;
}
// .single-image {
// display: inline-block;
// width: 100%;
// max-height: 600px;
// .attachments {
// width: unset;
// max-height: unset;
// display: inherit;
// }
// }
.overlay-circle {
border-radius: 50%;
background-color: var(--dark_grey);
width: 40px;
height: 40px;
align-items: center;
display: flex;
border-width: 5px;
border-color: var(--play_button);
border-style: solid;
}
.overlay-triangle {
width: 0;
height: 0;
border-style: solid;
border-width: 12px 0 12px 17px;
border-color: transparent transparent transparent var(--play_button);
margin-left: 14px;
}
.media-gif {
display: table;
background-color: unset;
width: unset;
}
.media-body {
flex: 1;
padding: 0;
white-space: pre-wrap;
}

42
sass/tweet/poll.scss Normal file
View file

@ -0,0 +1,42 @@
@import '_variables';
.poll-meter {
overflow: hidden;
position: relative;
margin: 6px 0;
height: 26px;
background: var(--bg_color);
border-radius: 5px;
display: flex;
align-items: center;
}
.poll-choice-bar {
height: 100%;
position: absolute;
background: var(--dark_grey);
}
.poll-choice-value {
position: relative;
font-weight: bold;
margin-left: 5px;
margin-right: 6px;
min-width: 30px;
text-align: right;
pointer-events: all;
}
.poll-choice-option {
position: relative;
pointer-events: all;
}
.poll-info {
color: var(--grey);
pointer-events: all;
}
.leader .poll-choice-bar {
background: var(--accent_dark);
}

94
sass/tweet/quote.scss Normal file
View file

@ -0,0 +1,94 @@
@import '_variables';
.quote {
margin-top: 10px;
border: solid 1px var(--dark_grey);
border-radius: 10px;
background-color: var(--bg_elements);
overflow: hidden;
pointer-events: all;
position: relative;
width: 100%;
&:hover {
border-color: var(--grey);
}
&.unavailable:hover {
border-color: var(--dark_grey);
}
.tweet-name-row {
padding: 6px 8px;
margin-top: 1px;
}
.quote-text {
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
padding: 0px 8px 8px 8px;
}
.show-thread {
padding: 0px 8px 6px 8px;
margin-top: -6px;
}
.replying-to {
padding: 0px 8px;
margin: unset;
}
}
.unavailable-quote {
padding: 12px;
}
.quote-link {
width: 100%;
height: 100%;
left: 0;
top: 0;
position: absolute;
}
.quote-media-container {
max-height: 300px;
display: flex;
.card {
margin: unset;
}
.attachments {
border-radius: 0;
}
.media-gif {
width: 100%;
display: flex;
justify-content: center;
}
.gallery-gif .attachment {
display: flex;
justify-content: center;
background-color: var(--bg_color);
video {
height: unset;
width: unset;
max-height: 100%;
max-width: 100%;
}
}
.gallery-video, .gallery-gif {
max-height: 300px;
}
.still-image img {
max-height: 250px
}
}

113
sass/tweet/thread.scss Normal file
View file

@ -0,0 +1,113 @@
@import '_variables';
@import '_mixins';
.conversation {
@include panel(100%, 600px);
.show-more {
margin-bottom: 10px;
}
}
.main-thread {
margin-bottom: 20px;
background-color: var(--bg_panel);
}
.main-tweet, .replies {
padding-top: 50px;
margin-top: -50px;
}
.main-tweet .tweet-content {
font-size: 18px;
}
@media(max-width: 600px) {
.main-tweet .tweet-content {
font-size: 16px;
}
}
.reply {
background-color: var(--bg_panel);
margin-bottom: 10px;
}
.thread-line {
.timeline-item::before,
&.timeline-item::before {
background: var(--accent_dark);
content: '';
position: relative;
min-width: 3px;
width: 3px;
left: 26px;
border-radius: 2px;
margin-left: -3px;
margin-bottom: 37px;
top: 56px;
z-index: 1;
pointer-events: none;
}
.with-header:not(:first-child)::after {
background: var(--accent_dark);
content: '';
position: relative;
float: left;
min-width: 3px;
width: 3px;
right: calc(100% - 26px);
border-radius: 2px;
margin-left: -3px;
margin-bottom: 37px;
bottom: 10px;
height: 30px;
z-index: 1;
pointer-events: none;
}
.unavailable::before {
top: 48px;
margin-bottom: 28px;
}
.more-replies::before {
content: '...';
background: unset;
color: var(--more_replies_dots);
font-weight: bold;
font-size: 20px;
line-height: 0.25em;
left: 1.2em;
width: 5px;
top: 2px;
margin-bottom: 0;
margin-left: -2.5px;
}
.earlier-replies {
padding-bottom: 0;
margin-bottom: -5px;
}
}
.timeline-item.thread-last::before {
background: unset;
min-width: unset;
width: 0;
margin: 0;
}
.more-replies {
padding-top: 0.3em !important;
}
.more-replies-text {
@include ellipsis;
display: block;
margin-left: 58px;
padding: 7px 0;
}

63
sass/tweet/video.scss Normal file
View file

@ -0,0 +1,63 @@
@import '_variables';
@import '_mixins';
video {
height: 100%;
width: 100%;
}
.gallery-video {
display: flex;
overflow: hidden;
}
.gallery-video.card-container {
flex-direction: column;
}
.video-container {
max-height: 530px;
margin: 0;
img {
height: 100%;
width: 100%;
}
}
.video-overlay {
@include play-button;
background-color: $shadow;
p {
position: relative;
z-index: 0;
text-align: center;
top: calc(50% - 20px);
font-size: 20px;
line-height: 1.3;
margin: 0 20px;
}
div {
position: relative;
z-index: 0;
top: calc(50% - 20px);
margin: 0 auto;
width: 40px;
height: 40px;
}
form {
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
display: flex;
}
button {
padding: 5px 8px;
font-size: 16px;
}
}

2
scripts/generate_scss.sh Normal file
View file

@ -0,0 +1,2 @@
#!/bin/bash
sassc -Isass/include sass/index.scss static/css/style.css

70
src/api/mod.rs Normal file
View file

@ -0,0 +1,70 @@
pub mod profile;
pub mod timeline;
mod token;
use std::fmt::Debug;
use std::time::Duration;
use reqwest::Url;
use serde::Deserialize;
use tokio::sync::RwLock;
use crate::consts::*;
use crate::errors::Result;
use self::token::Token;
pub struct Api {
client: reqwest::Client,
token: RwLock<Option<Token>>,
}
impl Api {
pub fn new() -> Result<Self> {
let client = reqwest::ClientBuilder::new()
.gzip(true)
.use_rustls_tls()
.timeout(Duration::new(15, 0))
.referer(false)
.build()?;
let token = RwLock::new(None);
Ok(Api { client, token })
}
async fn fetch<T>(&self, url: Url) -> Result<T>
where
T: for<'de> Deserialize<'de> + Debug,
{
let token = self.get_token().await?;
println!("fetching from {}", url);
use std::{fs::File, io::Write};
let mut file = File::create(format!("fetched-{}.json", url.as_str().replace("/", "-")))?;
let resp = self
.client
.get(url)
.header("Connection", "keep-alive")
.header("Authorization", AUTH)
.header("Content-Type", "application/json")
.header("X-Guest-Token", &token.token)
.header("X-Twitter-Active-User", "yes")
.header("Authority", "api.twitter.com")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept", "*/*")
.header("DNT", "1")
.send()
.await?;
let text = resp.text().await?;
println!("text: {:?}", text);
file.write(text.as_bytes())?;
std::mem::drop(file);
let body: T = serde_json::from_str(&text)?;
println!("body: {:?}", body);
Ok(body)
}
}

38
src/api/profile.rs Normal file
View file

@ -0,0 +1,38 @@
use reqwest::Url;
use crate::consts::*;
use crate::errors::Result;
use super::Api;
#[derive(Debug, Serialize, Deserialize)]
pub struct Profile {
#[serde(rename = "id_str")]
pub id: String,
pub name: String,
pub screen_name: String,
pub location: String,
pub description: String,
pub url: String,
pub followers_count: usize,
pub friends_count: usize,
#[serde(rename = "favourites_count")]
pub likes_count: usize,
#[serde(rename = "statuses_count")]
pub tweets_count: usize,
pub media_count: usize,
#[serde(skip)]
pub protected: bool,
#[serde(skip)]
pub verified: bool,
}
impl Api {
pub async fn fetch_profile(&self, username: impl AsRef<str>) -> Result<Profile> {
let username = username.as_ref();
let url = Url::parse_with_params(USER_SHOW_URL, &[("screen_name", username)])?;
let profile = self.fetch::<Profile>(url).await?;
Ok(profile)
}
}

58
src/api/timeline.rs Normal file
View file

@ -0,0 +1,58 @@
use std::collections::HashMap;
use reqwest::Url;
use crate::consts::*;
use crate::errors::Result;
use super::profile::Profile;
use super::Api;
#[derive(Debug, Serialize, Deserialize)]
pub struct Timeline {
#[serde(rename = "globalObjects")]
pub global_objects: GlobalObjects,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GlobalObjects {
pub tweets: HashMap<String, Tweet>,
pub users: HashMap<String, Profile>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Tweet {
#[serde(rename = "id_str")]
pub id: String,
pub created_at: String,
pub text: String,
pub source: String,
#[serde(rename = "user_id_str")]
pub user_id: String,
#[serde(skip)]
pub is_quote_status: bool,
#[serde(rename = "quoted_status_id_str")]
pub quoted_status_id: Option<String>,
pub retweet_count: usize,
#[serde(rename = "favorite_count")]
pub likes_count: usize,
#[serde(rename = "conversation_id_str")]
pub conversation_id: String,
}
impl Api {
pub async fn fetch_timeline(&self, id: impl AsRef<str>, replies: bool) -> Result<Timeline> {
let id = id.as_ref();
let replies = match replies {
true => "1",
false => "0",
};
let url = Url::parse_with_params(
&format!("{}/{}.json", TIMELINE_STUB, id),
&[("userId", id), ("include_tweet_replies", replies)],
)?;
let timeline = self.fetch::<Timeline>(url).await?;
Ok(timeline)
}
}

63
src/api/token.rs Normal file
View file

@ -0,0 +1,63 @@
use std::time::{Duration, Instant};
use crate::consts::*;
use crate::errors::Result;
use super::Api;
pub const RESET_PERIOD: u64 = 15 * 60;
#[derive(Clone)]
pub struct Token {
pub token: String,
reset: Instant,
}
impl Api {
pub async fn get_token(&self) -> Result<Token> {
if let Some(current_token) = &*self.token.read().await {
let now = Instant::now();
if current_token.reset > now {
return Ok(current_token.clone());
}
}
let token = self.fetch_token().await?;
*self.token.write().await = Some(token.clone());
return Ok(token);
}
async fn fetch_token(&self) -> Result<Token> {
let resp = self
.client
.post(ACTIVATE_URL)
.header(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
)
.header("Accept-Language", "en-US,en;q=0.5")
.header("User-Agent", generate_user_agent())
.header("Authorization", AUTH)
.send()
.await?;
#[derive(Deserialize)]
struct Response {
guest_token: String,
}
let response: Response = resp.json().await?;
let now = Instant::now();
let reset = now + Duration::new(RESET_PERIOD, 0);
let token = Token {
token: response.guest_token,
reset,
};
Ok(token)
}
}
fn generate_user_agent() -> String {
String::new()
}

10
src/config.rs Normal file
View file

@ -0,0 +1,10 @@
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
bind_addr: String,
port: u16,
http_max_connections: usize,
title: String,
hostname: String,
static_dir: String,
}

6
src/consts.rs Normal file
View file

@ -0,0 +1,6 @@
// TODO: no idea how this was obtained, will check back with twitter developer site later
pub const AUTH: &str = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
pub const ACTIVATE_URL: &str = "https://api.twitter.com/1.1/guest/activate.json";
pub const USER_SHOW_URL: &str = "https://api.twitter.com/1.1/users/show.json";
pub const TIMELINE_STUB: &str = "https://api.twitter.com/2/timeline/profile";

32
src/errors.rs Normal file
View file

@ -0,0 +1,32 @@
use rocket::{
http::Status,
request::Request,
response::{self, Responder, Response},
};
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Error)]
pub enum Error {
#[error("rate limit exceeded")]
RateLimitExceeded,
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("url parse error: {0}")]
UrlParse(#[from] url::ParseError),
#[error("serde_json error: {0}")]
Json(#[from] serde_json::Error),
#[error("generic io error: {0}")]
Io(#[from] std::io::Error),
}
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
error!("Error: {:?}", self);
Response::build().status(Status::InternalServerError).ok()
}
}

57
src/main.rs Normal file
View file

@ -0,0 +1,57 @@
#![doc = include_str!("../README.md")]
#![recursion_limit = "512"]
#[macro_use]
extern crate serde;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate thiserror;
#[macro_use]
extern crate typed_html;
mod api;
mod config;
mod consts;
mod errors;
mod render;
mod routes;
mod views;
use clap::Clap;
use rocket::fs::{FileServer, relative};
use crate::api::Api;
#[derive(Clap)]
struct Opt {
#[clap(short = 'v', long = "verbose", parse(from_occurrences))]
verbose: usize,
}
#[get("/favicon.ico")]
fn favicon() {}
#[launch]
fn rocket() -> _ {
let opt = Opt::parse();
// stderrlog::new()
// .module(module_path!())
// .verbosity(opt.verbose)
// .init()
// .unwrap();
use routes::timeline;
let api = Api::new().unwrap();
rocket::build()
.mount("/static", FileServer::from(relative!("/static")))
.mount(
"/",
routes![
favicon,
timeline::fetch_single_timeline,
timeline::fetch_single_timeline_tab,
],
)
.manage(api)
}

1
src/render.rs Normal file
View file

@ -0,0 +1 @@
pub fn render_main() {}

1
src/routes/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod timeline;

68
src/routes/timeline.rs Normal file
View file

@ -0,0 +1,68 @@
use rocket::{request::FromParam, State, response::content};
use typed_html::dom::DOMTree;
use crate::api::Api;
use crate::errors::Result;
use crate::views::timeline as view;
pub type FetchSingleTimelineReturn = Result<content::Html<String>>;
#[get("/<name>")]
pub async fn fetch_single_timeline(api: &State<Api>, name: String) -> FetchSingleTimelineReturn {
fetch_single_timeline_private(api, name, Tab::Main).await
}
#[get("/<name>/<tab>")]
pub async fn fetch_single_timeline_tab(
api: &State<Api>,
name: String,
tab: Tab,
) -> FetchSingleTimelineReturn {
fetch_single_timeline_private(api, name, tab).await
}
async fn fetch_single_timeline_private(
api: &State<Api>,
name: String,
_tab: Tab,
) -> FetchSingleTimelineReturn {
let profile = api.fetch_profile(name).await?;
let timeline = api.fetch_timeline(profile.id, false).await?;
println!("timeline: {:?}", timeline);
let rendered: DOMTree<String> = html!(
<html>
<head>
<title>"shitter"</title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
{ view::render_timeline_tweets(timeline.global_objects.tweets.values()) }
</body>
</html>
);
Ok(content::Html(rendered.to_string()))
}
#[derive(Debug)]
pub enum Tab {
Main,
WithReplies,
Media,
Search,
}
impl<'a> FromParam<'a> for Tab {
type Error = ();
fn from_param(string: &'a str) -> Result<Self, Self::Error> {
match string.to_lowercase().as_str() {
"" => Ok(Tab::Main),
"with_replies" => Ok(Tab::WithReplies),
"media" => Ok(Tab::Media),
"search" => Ok(Tab::Search),
_ => Err(()),
}
}
}

3
src/tokens.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn get_token() {
}

19
src/views/mod.rs Normal file
View file

@ -0,0 +1,19 @@
pub mod timeline;
pub mod tweet;
use std::convert::TryInto;
use typed_html::{
dom::DOMTree,
types::{Class, SpacedSet},
};
pub fn icon(icon: &str) -> DOMTree<String> {
let class_name = format!("icon-{}", icon);
let class_set: SpacedSet<Class> = [class_name.as_str()].try_into().unwrap();
html! (
<div class="icon-container">
<span class={ class_set }></span>
</div>
)
}

24
src/views/timeline.rs Normal file
View file

@ -0,0 +1,24 @@
use typed_html::{dom::DOMTree, elements::FlowContent};
use crate::api::timeline::Tweet;
use super::tweet::render_tweet;
pub fn render_to_top() -> Box<dyn FlowContent<String>> {
html!(
<div class="top-ref">
</div>
)
}
pub fn render_timeline_tweets<'a>(tweets: impl Iterator<Item = &'a Tweet>) -> Box<dyn FlowContent<String>> {
let tweets_rendered = tweets.map(|tweet| render_tweet(tweet));
html!(
<div class="timeline">
{ tweets_rendered }
{ render_to_top() }
</div>
)
}

13
src/views/tweet.rs Normal file
View file

@ -0,0 +1,13 @@
use typed_html::elements::FlowContent;
use crate::api::timeline::Tweet;
pub fn render_tweet(tweet: &Tweet) -> Box<dyn FlowContent<String>> {
html!(
<div class="timeline-item">
<div class="tweet-body">
{ text!(&tweet.text) }
</div>
</div>
)
}

1
static/css/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
style.css