Initial
Nix build doesn't work, blocked on https://github.com/kolloch/crate2nix/issues/207
This commit is contained in:
commit
ca1cdc8179
43 changed files with 5126 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
2495
Cargo.lock
generated
Normal file
2495
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal 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
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
shitter: Twitter Client
|
||||
===
|
||||
|
||||
License: AGPL
|
12
default.nix
Normal file
12
default.nix
Normal 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
125
flake.lock
Normal 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
43
flake.nix
Normal 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
39
sass/general.scss
Normal 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
97
sass/include/_mixins.css
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
44
sass/include/_variables.scss
Normal file
44
sass/include/_variables.scss
Normal 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
164
sass/index.scss
Normal 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
185
sass/inputs.scss
Normal 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
88
sass/navbar.scss
Normal 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
73
sass/profile/_base.scss
Normal 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
124
sass/profile/card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
97
sass/profile/photo-rail.scss
Normal file
97
sass/profile/photo-rail.scss
Normal 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
120
sass/search.scss
Normal 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
161
sass/timeline.scss
Normal 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
205
sass/tweet/_base.scss
Normal 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
118
sass/tweet/card.scss
Normal 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
17
sass/tweet/embed.scss
Normal 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
119
sass/tweet/media.scss
Normal 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
42
sass/tweet/poll.scss
Normal 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
94
sass/tweet/quote.scss
Normal 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
113
sass/tweet/thread.scss
Normal 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
63
sass/tweet/video.scss
Normal 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
2
scripts/generate_scss.sh
Normal file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
sassc -Isass/include sass/index.scss static/css/style.css
|
70
src/api/mod.rs
Normal file
70
src/api/mod.rs
Normal 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
38
src/api/profile.rs
Normal 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
58
src/api/timeline.rs
Normal 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
63
src/api/token.rs
Normal 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
10
src/config.rs
Normal 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
6
src/consts.rs
Normal 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
32
src/errors.rs
Normal 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
57
src/main.rs
Normal 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
1
src/render.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub fn render_main() {}
|
1
src/routes/mod.rs
Normal file
1
src/routes/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod timeline;
|
68
src/routes/timeline.rs
Normal file
68
src/routes/timeline.rs
Normal 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
3
src/tokens.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub fn get_token() {
|
||||
|
||||
}
|
19
src/views/mod.rs
Normal file
19
src/views/mod.rs
Normal 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
24
src/views/timeline.rs
Normal 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
13
src/views/tweet.rs
Normal 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
1
static/css/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
style.css
|
Loading…
Reference in a new issue