Adding 8 different themes.

- Fixes #276
This commit is contained in:
Dessalines 2019-10-15 12:21:27 -07:00
parent 37d4f84787
commit c0821fcaa5
35 changed files with 372 additions and 87 deletions

View file

@ -0,0 +1 @@
alter table user_ drop column theme;

View file

@ -0,0 +1 @@
alter table user_ add column theme varchar(20) default 'darkly' not null;

View file

@ -21,6 +21,7 @@ pub struct Register {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct SaveUserSettings { pub struct SaveUserSettings {
show_nsfw: bool, show_nsfw: bool,
theme: String,
auth: String, auth: String,
} }
@ -162,6 +163,7 @@ impl Perform<LoginResponse> for Oper<Register> {
admin: data.admin, admin: data.admin,
banned: false, banned: false,
show_nsfw: data.show_nsfw, show_nsfw: data.show_nsfw,
theme: "darkly".into(),
}; };
// Create the user // Create the user
@ -252,6 +254,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
admin: read_user.admin, admin: read_user.admin,
banned: read_user.banned, banned: read_user.banned,
show_nsfw: data.show_nsfw, show_nsfw: data.show_nsfw,
theme: data.theme.to_owned(),
}; };
let updated_user = match User_::update(&conn, user_id, &user_form) { let updated_user = match User_::update(&conn, user_id, &user_form) {
@ -416,6 +419,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
admin: data.added, admin: data.added,
banned: read_user.banned, banned: read_user.banned,
show_nsfw: read_user.show_nsfw, show_nsfw: read_user.show_nsfw,
theme: read_user.theme,
}; };
match User_::update(&conn, data.user_id, &user_form) { match User_::update(&conn, data.user_id, &user_form) {
@ -474,6 +478,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
admin: read_user.admin, admin: read_user.admin,
banned: data.ban, banned: data.ban,
show_nsfw: read_user.show_nsfw, show_nsfw: read_user.show_nsfw,
theme: read_user.theme,
}; };
match User_::update(&conn, data.user_id, &user_form) { match User_::update(&conn, data.user_id, &user_form) {

View file

@ -72,6 +72,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let person = expected_user.person(); let person = expected_user.person();

View file

@ -178,6 +178,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -264,6 +264,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -264,6 +264,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -446,6 +446,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_mod = User_::create(&conn, &new_mod).unwrap(); let inserted_mod = User_::create(&conn, &new_mod).unwrap();
@ -460,6 +461,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -191,6 +191,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -225,6 +225,7 @@ mod tests {
admin: false, admin: false,
banned: false, banned: false,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -20,6 +20,7 @@ pub struct User_ {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool, pub show_nsfw: bool,
pub theme: String,
} }
#[derive(Insertable, AsChangeset, Clone)] #[derive(Insertable, AsChangeset, Clone)]
@ -34,6 +35,7 @@ pub struct UserForm {
pub email: Option<String>, pub email: Option<String>,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool, pub show_nsfw: bool,
pub theme: String,
} }
impl Crud<UserForm> for User_ { impl Crud<UserForm> for User_ {
@ -74,6 +76,7 @@ pub struct Claims {
pub username: String, pub username: String,
pub iss: String, pub iss: String,
pub show_nsfw: bool, pub show_nsfw: bool,
pub theme: String,
} }
impl Claims { impl Claims {
@ -94,6 +97,7 @@ impl User_ {
username: self.name.to_owned(), username: self.name.to_owned(),
iss: self.fedi_name.to_owned(), iss: self.fedi_name.to_owned(),
show_nsfw: self.show_nsfw, show_nsfw: self.show_nsfw,
theme: self.theme.to_owned(),
}; };
encode( encode(
&Header::default(), &Header::default(),
@ -141,6 +145,7 @@ mod tests {
banned: false, banned: false,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -158,6 +163,7 @@ mod tests {
published: inserted_user.published, published: inserted_user.published,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
theme: "darkly".into(),
}; };
let read_user = User_::read(&conn, inserted_user.id).unwrap(); let read_user = User_::read(&conn, inserted_user.id).unwrap();

View file

@ -254,6 +254,7 @@ table! {
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
show_nsfw -> Bool, show_nsfw -> Bool,
theme -> Varchar,
} }
} }
@ -304,28 +305,28 @@ joinable!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id)); joinable!(user_ban -> user_ (user_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
category, category,
comment, comment,
comment_like, comment_like,
comment_saved, comment_saved,
community, community,
community_follower, community_follower,
community_moderator, community_moderator,
community_user_ban, community_user_ban,
mod_add, mod_add,
mod_add_community, mod_add_community,
mod_ban, mod_ban,
mod_ban_from_community, mod_ban_from_community,
mod_lock_post, mod_lock_post,
mod_remove_comment, mod_remove_comment,
mod_remove_community, mod_remove_community,
mod_remove_post, mod_remove_post,
mod_sticky_post, mod_sticky_post,
post, post,
post_like, post_like,
post_read, post_read,
post_saved, post_saved,
site, site,
user_, user_,
user_ban, user_ban,
); );

View file

@ -1,7 +1,3 @@
body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
color: #dedede !important;
}
.navbar-toggler { .navbar-toggler {
border: 0px; border: 0px;
} }
@ -31,29 +27,6 @@ body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
margin-top: -10px; margin-top: -10px;
} }
.form-control, .form-control:focus {
background-color: var(--secondary);
color: #fff;
}
.form-control:disabled {
background-color: var(--secondary);
opacity: .5;
}
.custom-select {
color: #fff;
background-color: var(--secondary);
}
.mark {
background-color: #333;
}
.mark-two {
background-color: #444 !important;
}
.md-div p:last-child { .md-div p:last-child {
margin-bottom: 0px; margin-bottom: 0px;
} }

83
ui/assets/css/themes/cyborg.min.css vendored Normal file

File diff suppressed because one or more lines are too long

35
ui/assets/css/themes/darkly.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
ui/assets/css/themes/journal.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
ui/assets/css/themes/litera.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
ui/assets/css/themes/minty.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
ui/assets/css/themes/sketchy.min.css vendored Normal file

File diff suppressed because one or more lines are too long

68
ui/assets/css/themes/solar.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
ui/assets/css/themes/united.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
ui/package.json vendored
View file

@ -21,6 +21,7 @@
"@types/markdown-it": "^0.0.7", "@types/markdown-it": "^0.0.7",
"@types/markdown-it-container": "^2.0.2", "@types/markdown-it-container": "^2.0.2",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"bootswatch": "^4.3.1",
"classcat": "^1.1.3", "classcat": "^1.1.3",
"dotenv": "^6.1.0", "dotenv": "^6.1.0",
"emoji-short-name": "^0.1.0", "emoji-short-name": "^0.1.0",

View file

@ -13,7 +13,7 @@ export class Footer extends Component<any, any> {
render() { render() {
return ( return (
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3 my-2"> <nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3 mt-2">
<div className="navbar-collapse"> <div className="navbar-collapse">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">

View file

@ -96,8 +96,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div className="post-title"> <div className="post-title">
<h5 className="mb-0 d-inline"> <h5 className="mb-0 d-inline">
{post.url ? {post.url ?
<a className="text-white" href={post.url} target="_blank" title={post.url}>{post.name}</a> : <a className="text-body" href={post.url} target="_blank" title={post.url}>{post.name}</a> :
<Link className="text-white" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link> <Link className="text-body" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link>
} }
</h5> </h5>
{post.url && {post.url &&

View file

@ -74,7 +74,7 @@ export class Post extends Component<any, PostState> {
if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) { if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`); var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
elmnt.scrollIntoView(); elmnt.scrollIntoView();
elmnt.classList.add("mark-two"); elmnt.classList.add("mark");
this.state.scrolled = true; this.state.scrolled = true;
this.markScrolledAsRead(this.state.scrolled_comment_id); this.markScrolledAsRead(this.state.scrolled_comment_id);
} }

View file

@ -4,7 +4,7 @@ import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse, BanUserResponse, AddAdminResponse } from '../interfaces'; import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse, BanUserResponse, AddAdminResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils'; import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter, themes, setTheme } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
@ -61,6 +61,7 @@ export class User extends Component<any, UserState> {
page: this.getPageFromProps(this.props), page: this.getPageFromProps(this.props),
userSettingsForm: { userSettingsForm: {
show_nsfw: null, show_nsfw: null,
theme: null,
auth: null, auth: null,
}, },
userSettingsLoading: null, userSettingsLoading: null,
@ -285,7 +286,18 @@ export class User extends Component<any, UserState> {
<div class="card-body"> <div class="card-body">
<h5><T i18nKey="settings">#</T></h5> <h5><T i18nKey="settings">#</T></h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}> <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group row"> <div class="form-group">
<div class="col-12">
<label><T i18nKey="theme">#</T></label>
<select value={this.state.userSettingsForm.theme} onChange={linkEvent(this, this.handleUserSettingsThemeChange)} class="ml-2 custom-select custom-select-sm w-auto">
<option disabled><T i18nKey="theme">#</T></option>
{themes.map(theme =>
<option value={theme}>{theme}</option>
)}
</select>
</div>
</div>
<div class="form-group">
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/> <input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/>
@ -309,20 +321,18 @@ export class User extends Component<any, UserState> {
moderates() { moderates() {
return ( return (
<div> <div>
<div class="card border-secondary mb-3"> {this.state.moderates.length > 0 &&
<div class="card-body"> <div class="card border-secondary mb-3">
{this.state.moderates.length > 0 && <div class="card-body">
<div> <h5><T i18nKey="moderates">#</T></h5>
<h5><T i18nKey="moderates">#</T></h5> <ul class="list-unstyled mb-0">
<ul class="list-unstyled mb-0"> {this.state.moderates.map(community =>
{this.state.moderates.map(community => <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> )}
)} </ul>
</ul> </div>
</div>
}
</div> </div>
</div> }
</div> </div>
) )
} }
@ -410,6 +420,12 @@ export class User extends Component<any, UserState> {
i.setState(i.state); i.setState(i.state);
} }
handleUserSettingsThemeChange(i: User, event: any) {
i.state.userSettingsForm.theme = event.target.value;
setTheme(event.target.value);
i.setState(i.state);
}
handleUserSettingsSubmit(i: User, event: any) { handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault(); event.preventDefault();
i.state.userSettingsLoading = true; i.state.userSettingsLoading = true;
@ -435,6 +451,7 @@ export class User extends Component<any, UserState> {
this.state.loading = false; this.state.loading = false;
if (this.isCurrentUser) { if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw; this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw;
this.state.userSettingsForm.theme = UserService.Instance.user.theme ? UserService.Instance.user.theme : 'darkly';
} }
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`; document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0,0); window.scrollTo(0,0);

File diff suppressed because one or more lines are too long

16
ui/src/index.html vendored
View file

@ -6,8 +6,24 @@
<meta name="Description" content="Lemmy"> <meta name="Description" content="Lemmy">
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Icons -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" /> <link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
<link rel="apple-touch-icon" href="/static/assets/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/static/assets/apple-touch-icon.png" />
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/litera.min.css" id="litera" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/minty.min.css" id="minty" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/solar.min.css" id="solar" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/united.min.css" id="united" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/cyborg.min.css" id="cyborg" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/journal.min.css" id="journal" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/sketchy.min.css" id="sketchy" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
<!-- Scripts -->
<script async src="/static/assets/libs/sortable/sortable.min.js"></script> <script async src="/static/assets/libs/sortable/sortable.min.js"></script>
</head> </head>

4
ui/src/index.tsx vendored
View file

@ -19,10 +19,6 @@ import { Sponsors } from './components/sponsors';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import { i18n } from './i18next'; import { i18n } from './i18next';
import './css/tribute.css';
import './css/bootstrap.min.css';
import './css/main.css';
import { WebSocketService, UserService } from './services'; import { WebSocketService, UserService } from './services';
const container = document.getElementById('app'); const container = document.getElementById('app');

View file

@ -23,6 +23,7 @@ export interface User {
iss: string; iss: string;
username: string; username: string;
show_nsfw: boolean; show_nsfw: boolean;
theme: string;
} }
export interface UserView { export interface UserView {
@ -381,6 +382,7 @@ export interface LoginResponse {
export interface UserSettingsForm { export interface UserSettingsForm {
show_nsfw: boolean; show_nsfw: boolean;
theme: string;
auth: string; auth: string;
} }

View file

@ -1,5 +1,6 @@
import * as Cookies from 'js-cookie'; import * as Cookies from 'js-cookie';
import { User, LoginResponse } from '../interfaces'; import { User, LoginResponse } from '../interfaces';
import { setTheme } from '../utils';
import * as jwt_decode from 'jwt-decode'; import * as jwt_decode from 'jwt-decode';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -14,6 +15,7 @@ export class UserService {
if (jwt) { if (jwt) {
this.setUser(jwt); this.setUser(jwt);
} else { } else {
setTheme();
console.log('No JWT cookie found.'); console.log('No JWT cookie found.');
} }
} }
@ -27,8 +29,9 @@ export class UserService {
public logout() { public logout() {
this.user = undefined; this.user = undefined;
Cookies.remove("jwt"); Cookies.remove("jwt");
console.log("Logged out."); setTheme();
this.sub.next({user: undefined, unreadCount: 0}); this.sub.next({user: undefined, unreadCount: 0});
console.log("Logged out.");
} }
public get auth(): string { public get auth(): string {
@ -37,6 +40,7 @@ export class UserService {
private setUser(jwt: string) { private setUser(jwt: string) {
this.user = jwt_decode(jwt); this.user = jwt_decode(jwt);
setTheme(this.user.theme);
this.sub.next({user: this.user, unreadCount: 0}); this.sub.next({user: this.user, unreadCount: 0});
console.log(this.user); console.log(this.user);
} }

View file

@ -129,6 +129,7 @@ export const en = {
modified: 'modified', modified: 'modified',
nsfw: 'NSFW', nsfw: 'NSFW',
show_nsfw: 'Show NSFW content', show_nsfw: 'Show NSFW content',
theme: 'Theme',
sponsors: 'Sponsors', sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors of Lemmy', sponsors_of_lemmy: 'Sponsors of Lemmy',
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:', sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',

14
ui/src/utils.ts vendored
View file

@ -238,3 +238,17 @@ export function getMomentLanguage(): string {
} }
return lang; return lang;
} }
export const themes = ['litera', 'minty', 'solar', 'united', 'cyborg','darkly', 'journal', 'sketchy'];
export function setTheme(theme: string = 'darkly') {
for (var i=0; i < themes.length; i++) {
let styleSheet = document.getElementById(themes[i]);
if (themes[i] == theme) {
styleSheet.removeAttribute("disabled");
} else {
styleSheet.setAttribute("disabled", "disabled");
}
}
}

5
ui/yarn.lock vendored
View file

@ -381,6 +381,11 @@ body@^5.1.0:
raw-body "~1.1.0" raw-body "~1.1.0"
safe-json-parse "~1.0.1" safe-json-parse "~1.0.1"
bootswatch@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/bootswatch/-/bootswatch-4.3.1.tgz#be54748b420a1962dbcf9782605aac092f842e38"
integrity sha512-kNdpo/TnhO++aic1IODLIe1V0lx6pXwHMpwXMacpANDnuVDtgU1MUgUbVMC3rSWm4UcbImfwPraNYgjKDT0BtA==
bowser@^2.0.0-beta.3: bowser@^2.0.0-beta.3:
version "2.6.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.6.1.tgz#196599588af6f0413449c79ab3bf7a5a1bb3384f" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.6.1.tgz#196599588af6f0413449c79ab3bf7a5a1bb3384f"