+3
-1
config.maple
+3
-1
config.maple
···
18
18
19
19
// TODO: Move default_theme and allow_changing_theme to user settings
20
20
// Default theme applied for all users.
21
-
default_theme = 'https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css'
21
+
default_theme = '/static/themes/default.css'
22
+
// Default custom CSS applied for all users.
23
+
default_css = ''
22
24
// Whether or not users should be able to change their theme.
23
25
allow_changing_theme = true
24
26
+3
doc/database_spec.md
+3
doc/database_spec.md
···
20
20
| `admin` | bool | controls whether or not this user is an admin |
21
21
| `automated` | bool | controls whether or not this user is automated |
22
22
| `theme` | ?string | controls per-user css themes |
23
+
| `css` | ?string | controls per-user css |
23
24
| `bio` | string | bio for this user |
24
25
| `pronouns` | string | pronouns for this user |
25
26
| `created_at` | time.Time | a timestamp of when this user was made |
···
35
36
| `replying_to` | ?int | id of the post that this post is replying to |
36
37
| `title` | string | the title of this post |
37
38
| `body` | string | the body of this post |
39
+
| `pinned` | bool | if this post in globally pinned |
40
+
| `nsfw` | bool | if this post in marked as nsfw |
38
41
| `posted_at` | time.Time | a timestamp of when this post was made |
39
42
40
43
## `Like`
+5
-6
doc/themes.md
+5
-6
doc/themes.md
···
30
30
31
31
## beep-specific
32
32
33
-
| name | source | css theme url |
34
-
|------|--------|---------------|
35
-
| | | |
36
-
37
-
> there is nothing here yet! do you want to be the one to change that?
33
+
| name | source | css theme url |
34
+
|---------|----------------------------------------------------|----------------------------|
35
+
| default | <https://tangled.org/emmeline.girlkisser.top/beep> | /static/themes/default.css |
38
36
39
37
## built-in
40
38
41
39
| name | based on (if applicable) | css theme url |
42
40
|-----------------------------|---------------------------------|---------------------------------|
41
+
| default | n/a | default.css |
43
42
| catppuccin-macchiato-pink | water.css + catpuccin macchiato | catppuccin-macchiato-pink.css |
44
43
| catppuccin-macchiato-green | water.css + catpuccin macchiato | catppuccin-macchiato-green.css |
45
44
| catppuccin-macchiato-yellow | water.css + catpuccin macchiato | catppuccin-macchiato-yellow.css |
···
48
47
> beep also features some built-in themes, some of which are based on the themes
49
48
> present in the "it just works" list!
50
49
51
-
> make sure to prefix the url with `<instance url>/static/themes/`
50
+
> make sure to prefix the url with `/static/themes/`
+12
src/database/user.v
+12
src/database/user.v
···
90
90
return true
91
91
}
92
92
93
+
// set_css sets the given user's custom CSS, returns true if this succeeded and
94
+
// false otherwise.
95
+
pub fn (app &DatabaseAccess) set_css(user_id int, css ?string) bool {
96
+
sql app.db {
97
+
update User set css = css where id == user_id
98
+
} or {
99
+
eprintln('failed to update css for ${user_id}')
100
+
return false
101
+
}
102
+
return true
103
+
}
104
+
93
105
// set_pronouns sets the given user's pronouns, returns true if this succeeded
94
106
// and false otherwise.
95
107
pub fn (app &DatabaseAccess) set_pronouns(user_id int, pronouns string) bool {
+1
src/entity/user.v
+1
src/entity/user.v
+6
-1
src/static/style.css
+6
-1
src/static/style.css
···
1
+
:root {
2
+
--c-nsfw-border: red;
3
+
}
4
+
1
5
.post,
2
6
.notification {
3
7
border: 2px solid;
···
22
26
23
27
pre {
24
28
white-space: pre-wrap;
29
+
word-wrap: break-word;
25
30
}
26
31
27
32
span.nsfw-indicator {
28
-
border: 2px solid red;
33
+
border: 2px solid var(--c-nsfw-border);
29
34
border-radius: 2px;
30
35
padding-left: 4px;
31
36
padding-right: 4px;
+106
src/static/themes/default.css
+106
src/static/themes/default.css
···
1
+
@import url('https://fonts.googleapis.com/css2?family=Nova+Mono&family=Oxygen+Mono&display=swap');
2
+
3
+
:root {
4
+
--c-bg: #222;
5
+
--c-panel-bg: #2c2c2c;
6
+
--c-fg: #ffffff;
7
+
--c-nsfw-border: #ff6666;
8
+
--c-link: #6666ff;
9
+
--c-accent: #faaeff;
10
+
}
11
+
12
+
html {
13
+
padding: 0;
14
+
offset: 0;
15
+
margin: 0;
16
+
17
+
width: 100vw;
18
+
19
+
display: flex;
20
+
flex-direction: column;
21
+
align-items: center;
22
+
23
+
background-color: var(--c-bg);
24
+
color: var(--c-fg);
25
+
26
+
font-family: "Oxygen Mono", sans-serif;
27
+
font-weight: 400;
28
+
font-style: normal;
29
+
font-size: 20px;
30
+
}
31
+
32
+
body {
33
+
padding: 16px 0 0 0;
34
+
offset: 0;
35
+
margin: 0;
36
+
37
+
width: 80vw;
38
+
}
39
+
40
+
header {
41
+
padding-bottom: 16px;
42
+
}
43
+
44
+
footer {
45
+
padding-top: 16px;
46
+
}
47
+
48
+
main {
49
+
padding: 16px;
50
+
51
+
background-color: var(--c-panel-bg);
52
+
53
+
display: flex;
54
+
flex-direction: column;
55
+
gap: 12px;
56
+
}
57
+
58
+
form {
59
+
display: flex;
60
+
flex-direction: column;
61
+
gap: 12px;
62
+
}
63
+
64
+
input,
65
+
textarea,
66
+
button {
67
+
background-color: var(--c-panel-bg);
68
+
color: var(--c-fg);
69
+
70
+
border: 2px solid var(--c-accent);
71
+
padding: 6px;
72
+
}
73
+
74
+
h1, h2, h3, h4, h5, h6, p {
75
+
margin: 0;
76
+
}
77
+
78
+
h1, header, footer {
79
+
font-family: "Nova Mono", sans-serif;
80
+
}
81
+
82
+
a {
83
+
color: var(--c-link);
84
+
}
85
+
86
+
hr {
87
+
width: 100%;
88
+
}
89
+
90
+
.post {
91
+
border: none;
92
+
border-left: 2px solid var(--c-fg);
93
+
}
94
+
95
+
.post + .post,
96
+
.notification + .notification {
97
+
margin-top: 18px;
98
+
}
99
+
100
+
form,
101
+
#recent-posts,
102
+
#pinned-posts {
103
+
border-top: 2px solid var(--c-accent);
104
+
border-bottom: 2px solid var(--c-accent);
105
+
padding: 16px 24px 16px 24px;
106
+
}
+9
-4
src/templates/components/new_post.html
+9
-4
src/templates/components/new_post.html
···
15
15
>
16
16
@end
17
17
18
-
<p id="title_chars">0/@{app.config.post.title_max_len}</p>
19
18
@if replying
20
19
<input
21
20
type="text"
···
27
26
hidden aria-hidden
28
27
>
29
28
@else
29
+
<label for="title" id="title_chars">0/@{app.config.post.title_max_len}</label>
30
+
<br>
30
31
<input
31
32
type="text"
32
33
name="title"
···
40
41
@end
41
42
<br>
42
43
43
-
<p id="body_chars">0/@{app.config.post.body_max_len}</p>
44
+
<label for="body" id="body_chars">0/@{app.config.post.body_max_len}</label>
45
+
<br>
44
46
<textarea
45
47
name="body"
46
48
id="body"
···
55
57
<br>
56
58
57
59
@if app.config.post.allow_nsfw
58
-
<label for="nsfw">is nsfw:</label>
59
-
<input type="checkbox" name="nsfw" id="nsfw" />
60
+
<div>
61
+
<label for="nsfw">is nsfw:</label>
62
+
<input type="checkbox" name="nsfw" id="nsfw" />
63
+
</div>
64
+
<br>
60
65
@else
61
66
<input type="checkbox" name="nsfw" id="nsfw" hidden aria-hidden />
62
67
@end
+4
-4
src/templates/index.html
+4
-4
src/templates/index.html
···
8
8
9
9
<div>
10
10
@if pinned_posts.len > 0
11
-
<h2>pinned posts:</h2>
12
-
<div>
11
+
<div id="pinned-posts">
12
+
<h2>pinned posts:</h2>
13
13
@for post in pinned_posts
14
14
@include 'components/post_small.html'
15
15
@end
···
17
17
<br>
18
18
@end
19
19
20
-
<h2>recent posts:</h2>
21
-
<div>
20
+
<div id="recent-posts">
21
+
<h2>recent posts:</h2>
22
22
@if recent_posts.len > 0
23
23
@for post in recent_posts
24
24
@include 'components/post_small.html'
+6
-1
src/templates/partial/header.html
+6
-1
src/templates/partial/header.html
···
6
6
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
<meta name="description" content="" />
8
8
9
-
<link rel="icon" href="/favicon.png" />
10
9
<title>@ctx.title</title>
11
10
12
11
@include 'assets/style.html'
···
18
17
@endif
19
18
20
19
<link rel="shortcut icon" href="/static/favicon/favicon.ico" type="image/png" sizes="16x16 32x32">
20
+
21
+
@if ctx.is_logged_in() && user.css != ''
22
+
<style>@{user.css}</style>
23
+
@else
24
+
<style>@{app.config.instance.default_css}</style>
25
+
@end
21
26
</head>
22
27
23
28
<body>
+8
-3
src/templates/post.html
+8
-3
src/templates/post.html
···
3
3
<script src="/static/js/post.js"></script>
4
4
<script src="/static/js/render_body.js"></script>
5
5
6
+
<br>
7
+
6
8
<div class="post post-full">
7
9
<h2>
8
-
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}">
9
-
<strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong>
10
-
</a>
10
+
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>
11
11
-
12
12
@if replying_to_post.id == 0
13
13
@post.title
···
19
19
@end
20
20
</h2>
21
21
22
+
<hr>
23
+
22
24
@if post.nsfw
23
25
<details>
24
26
<summary>click to show post (nsfw)</summary>
···
28
30
<pre id="post-@{post.id}">@post.body</pre>
29
31
@end
30
32
33
+
<hr>
34
+
31
35
<p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p>
32
36
<p><em>posted at: @post.posted_at</em></p>
33
37
34
38
@if ctx.is_logged_in() && !user.automated
39
+
<br>
35
40
<p><a href="/post/@{post.id}/reply">reply</a></p>
36
41
<br>
37
42
<div>
+22
-11
src/templates/settings.html
+22
-11
src/templates/settings.html
···
67
67
68
68
<form action="/api/user/set_theme" method="post">
69
69
<label for="url">theme:</label>
70
-
<input type="url" name="url" id="url" value="@user.theme">
70
+
<input type="text" name="url" id="url" value="@user.theme">
71
+
<input type="submit" value="save">
72
+
</form>
73
+
74
+
<hr>
75
+
76
+
<form action="/api/user/set_css" method="post">
77
+
<label for="css">custom css:</label>
78
+
<br>
79
+
<textarea type="text" name="css" id="css" style="font: monospace;">@user.css</textarea>
71
80
<input type="submit" value="save">
72
81
</form>
73
82
@end
···
92
101
<hr>
93
102
94
103
<form action="/api/user/set_automated" method="post">
95
-
<label for="is_automated">is automated:</label>
96
-
<input
97
-
type="checkbox"
98
-
name="is_automated"
99
-
id="is_automated"
100
-
value="true"
101
-
@if user.automated
102
-
checked aria-checked
103
-
@end
104
-
>
104
+
<div>
105
+
<label for="is_automated">is automated:</label>
106
+
<input
107
+
type="checkbox"
108
+
name="is_automated"
109
+
id="is_automated"
110
+
value="true"
111
+
@if user.automated
112
+
checked aria-checked
113
+
@end
114
+
>
115
+
</div>
105
116
<input type="submit" value="save">
106
117
<p>automated accounts are primarily intended to tell users that this account makes posts automatically.</p>
107
118
<p>it will also hide most front-end interactions since the user of this account likely will not be using those very often.</p>
+25
-1
src/webapp/api.v
+25
-1
src/webapp/api.v
···
331
331
}
332
332
333
333
mut theme := ?string(none)
334
-
if url.trim_space() != '' {
334
+
if url.trim_space() == '' {
335
+
theme = app.config.instance.default_theme
336
+
} else {
335
337
theme = url.trim_space()
336
338
}
337
339
338
340
if !app.set_theme(user.id, theme) {
339
341
ctx.error('failed to change theme')
342
+
return ctx.redirect('/settings')
343
+
}
344
+
345
+
return ctx.redirect('/settings')
346
+
}
347
+
348
+
@['/api/user/set_css'; post]
349
+
fn (mut app App) api_user_set_css(mut ctx Context, css string) veb.Result {
350
+
if !app.config.instance.allow_changing_theme {
351
+
ctx.error('this instance disallows changing themes :(')
352
+
return ctx.redirect('/settings')
353
+
}
354
+
355
+
user := app.whoami(mut ctx) or {
356
+
ctx.error('you are not logged in!')
357
+
return ctx.redirect('/login')
358
+
}
359
+
360
+
c := if css.trim_space() == '' { app.config.instance.default_css } else { css.trim_space() }
361
+
362
+
if !app.set_css(user.id, c) {
363
+
ctx.error('failed to change css')
340
364
return ctx.redirect('/settings')
341
365
}
342
366
+2
src/webapp/config.v
+2
src/webapp/config.v
···
12
12
name string
13
13
welcome string
14
14
default_theme string
15
+
default_css string
15
16
allow_changing_theme bool
16
17
version string
17
18
source string
···
83
84
config.instance.name = loaded_instance.get('name').to_str()
84
85
config.instance.welcome = loaded_instance.get('welcome').to_str()
85
86
config.instance.default_theme = loaded_instance.get('default_theme').to_str()
87
+
config.instance.default_css = loaded_instance.get('default_css').to_str()
86
88
config.instance.allow_changing_theme = loaded_instance.get('allow_changing_theme').to_bool()
87
89
config.instance.version = loaded_instance.get('version').to_str()
88
90
config.instance.source = loaded_instance.get('source').to_str()