openwrite 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openwrite/__init__.py +134 -0
- openwrite/cli.py +340 -0
- openwrite/db/__init__.py +0 -0
- openwrite/db/base.py +3 -0
- openwrite/launch_gemini.py +8 -0
- openwrite/routes/admin.py +170 -0
- openwrite/routes/auth.py +77 -0
- openwrite/routes/blog.py +205 -0
- openwrite/routes/dashboard.py +408 -0
- openwrite/routes/federation.py +284 -0
- openwrite/routes/main.py +96 -0
- openwrite/routes/upload.py +81 -0
- openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- openwrite/static/Merriweather.ttf +0 -0
- openwrite/static/avatar.png +0 -0
- openwrite/static/favicon.ico +0 -0
- openwrite/static/logo.png +0 -0
- openwrite/static/script/chart.js +14 -0
- openwrite/static/script/highlight.min.js +1244 -0
- openwrite/static/script/openwrite.js +88 -0
- openwrite/static/script/papaparse.min.js +7 -0
- openwrite/static/script/showdown.min.js +3 -0
- openwrite/static/script/showdown.min.js.map +1 -0
- openwrite/static/script/site.compat.min.js +7 -0
- openwrite/static/script/site.min.js +7 -0
- openwrite/static/style/hl.css +1 -0
- openwrite/static/style/style.css +611 -0
- openwrite/static/style/themes/light.css +28 -0
- openwrite/static/style/themes/obscura.css +27 -0
- openwrite/static/style/themes/stickynote.css +50 -0
- openwrite/static/style/themes/terminal.css +37 -0
- openwrite/static/style/themes/warmnight.css +73 -0
- openwrite/static/style/themes/windows98.css +132 -0
- openwrite/templates/admin.html +47 -0
- openwrite/templates/blog.html +16 -0
- openwrite/templates/blog_footer.html +6 -0
- openwrite/templates/blog_header.html +45 -0
- openwrite/templates/blog_preview.html +44 -0
- openwrite/templates/changepw.html +19 -0
- openwrite/templates/create.html +46 -0
- openwrite/templates/dashboard.html +45 -0
- openwrite/templates/discover.html +11 -0
- openwrite/templates/edit.html +190 -0
- openwrite/templates/footer.html +16 -0
- openwrite/templates/header.html +61 -0
- openwrite/templates/import.html +142 -0
- openwrite/templates/index.html +9 -0
- openwrite/templates/instances.html +14 -0
- openwrite/templates/login.html +18 -0
- openwrite/templates/new_post.html +207 -0
- openwrite/templates/post.html +53 -0
- openwrite/templates/preview.html +37 -0
- openwrite/templates/register.html +42 -0
- openwrite/templates/settings.html +63 -0
- openwrite/templates/stats.html +242 -0
- openwrite/utils/create_db.py +15 -0
- openwrite/utils/db.py +30 -0
- openwrite/utils/helpers.py +216 -0
- openwrite/utils/i18n.json +229 -0
- openwrite/utils/models.py +87 -0
- openwrite/utils/translations.py +6 -0
- openwrite-0.10.1.dist-info/METADATA +684 -0
- openwrite-0.10.1.dist-info/RECORD +67 -0
- openwrite-0.10.1.dist-info/WHEEL +5 -0
- openwrite-0.10.1.dist-info/entry_points.txt +2 -0
- openwrite-0.10.1.dist-info/licenses/LICENSE +661 -0
- openwrite-0.10.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
{% if error %}
|
|
3
|
+
<div class="error">{{ error }}</div>
|
|
4
|
+
{% endif %}
|
|
5
|
+
<div><a href="/dashboard" class="action">Dashboard</a> > {{ blog.title }}</div>
|
|
6
|
+
<h1 class="centered">
|
|
7
|
+
{% if g.mode == "multi" %}
|
|
8
|
+
blog: {{ blog.name }}
|
|
9
|
+
{% else %}
|
|
10
|
+
{{ g.trans['edit'] }} blog
|
|
11
|
+
{% endif %}
|
|
12
|
+
</h1>
|
|
13
|
+
<div class="bloginfo">
|
|
14
|
+
|
|
15
|
+
<form method="post" action="/dashboard/edit/{{ blog.name }}">
|
|
16
|
+
<div class="container-row-fluid">
|
|
17
|
+
<div style="display: flex; flex-direction: column">
|
|
18
|
+
<span class="bloginfo">{{ g.trans['title'] }}: <input type="text" name="title" value="{{ blog.title }}"></span><br>
|
|
19
|
+
<span class="bloginfo">URL:
|
|
20
|
+
{% if g.mode == "multi" %}
|
|
21
|
+
<a href="{% if blog.access == "path" %}https://{{ g.main_domain }}/b/{{ blog.name }} {% else %}https://{{ blog.name }}.{{ g.main_domain }}/{% endif %}" target="_blank" style="text-decoration: none; color: #fff;">{% if blog.access == "path" %}https://{{ g.main_domain }}/b/{{ blog.name }} {% else %}https://{{ blog.name }}.{{ g.main_domain }}/{% endif %}</a>
|
|
22
|
+
{% else %}
|
|
23
|
+
<a href="http://{{ g.main_domain }}" target="_blank" style="text-decoration: none; color: #fff">{{ g.main_domain }}</a><br>
|
|
24
|
+
{% endif %}
|
|
25
|
+
</span><br>
|
|
26
|
+
<span class="bloginfo">{{ g.trans['select_theme'] }}:
|
|
27
|
+
<select name="theme" style="min-width: 14rem; min-height: 2rem; color: #fff; background: #211932;">
|
|
28
|
+
<option value="default" {% if blog.theme == "default" %}selected{% endif %}>{{ g.trans['default'] }}</option>
|
|
29
|
+
{% for t in themes %}
|
|
30
|
+
<option value="{{ t }}" {% if blog.theme == t %}selected{%endif %}>{{ t }}</option>
|
|
31
|
+
{% endfor %}
|
|
32
|
+
</select>
|
|
33
|
+
</span><a target="_blank" href="https://main.openwrite.io/available-themes" class="action">{{ g.trans['see_themes'] }}</a>
|
|
34
|
+
</div>
|
|
35
|
+
<a target="_blank" href="/dashboard/blog_preview" class="btn purple" id="preview">{{ g.trans['preview'] }}</a>
|
|
36
|
+
</div>
|
|
37
|
+
<span class="bloginfo">{{ g.trans['description'] }} (markdown): </span>
|
|
38
|
+
<textarea autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" name="content" class="post_content" placeholder="content" style="min-height: 200px; margin-bottom: 1rem; width: 100%">{{ blog.description_raw }}</textarea>
|
|
39
|
+
<div style="display: flex; flex-direction: column; margin-bottom: 1rem">
|
|
40
|
+
<span class="bloginfo">{{ g.trans['custom_css'] }}:</span>
|
|
41
|
+
<i style="margin-bottom: 1rem;">{{ g.trans['css_unsafe_info'] }}</i>
|
|
42
|
+
<textarea name="css" style="min-height: 100px;" class="post_content">{{ blog.css }}</textarea>
|
|
43
|
+
</div>
|
|
44
|
+
<div style="display: flex; justify-content: space-between">
|
|
45
|
+
<input type="submit" id="submit" value="{{ g.trans['save'] }}" class="btn purple">
|
|
46
|
+
{% if g.upload_enabled %}
|
|
47
|
+
<a href="#" id="uploadLink" class="btn empty">📎 {{ g.trans['upload_image'] }}</a>
|
|
48
|
+
<input type="file" id="fileInput" style="display: none;">
|
|
49
|
+
{% endif %}
|
|
50
|
+
</div>
|
|
51
|
+
</form>
|
|
52
|
+
|
|
53
|
+
<span class="bloginfo">{{ g.trans['posts'] }}:</span>
|
|
54
|
+
<table class="posts" border="1">
|
|
55
|
+
<thead>
|
|
56
|
+
<th width="20%">{{ g.trans['date'] }}</th><th>{{ g.trans['title'] }}</th><th width="5%">{{ g.trans['views'] }}<th width="15%">{{ g.trans['actions'] }}</th>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
{% for post in posts %}
|
|
60
|
+
<tr><td style="text-align: center" class="datetime">{{ post.date }}</td><td><a href="{% if blog.access == "path" %}https://{{ g.main_domain }}/b/{{ blog.name }}/{% else %}https://{{ blog.name }}.{{ g.main_domain }}/{% endif %}{{ post.link }}" class="action" target="_blank">{{ post.title }}</a></td><td style="text-align: center">{{ post.views }}</td><td style="display: flex; gap: 1rem; padding: 0.5rem; justify-content: space-between;"><a class="action" href="/dashboard/edit/{{ blog.name }}/{{ post.link }}">{{ g.trans['edit'] }}</a><a class="action" href="/dashboard/edit/{{ blog.name }}/{{ post.link }}/delete" onclick="return confirm('{{ g.trans['are_you_sure_post'] }} {{ post.title }}?\n{{ g.trans['this_action_cannot_be_reverted'] }}.')">{{ g.trans['delete'] }}</a></td></tr>
|
|
61
|
+
{% endfor %}
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
<div class="buttons-list">
|
|
65
|
+
<a href="/dashboard/post/{{ blog.name }}" class="btn purple">{{ g.trans['new_post'] }}</a>
|
|
66
|
+
<a href="/dashboard/delete/{{ blog.name }}" class="btn red" onclick="return confirm('{{ g.trans['are_you_sure_blog'] }} {{ blog.name }}?\n{{ g.trans['this_action_cannot_be_reverted'] }}.')">{{ g.trans['delete_blog'] }}</a>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<script nonce="{{ g.nonce }}">
|
|
70
|
+
var converter = new showdown.Converter();
|
|
71
|
+
converter.setOption('emoji', true);
|
|
72
|
+
converter.setOption('noHeaderId', true);
|
|
73
|
+
converter.setOption('openLinksInNewWindow', true);
|
|
74
|
+
converter.setOption('tables', true);
|
|
75
|
+
converter.setOption('tasklists', true);
|
|
76
|
+
converter.setOption('strikethrough', true);
|
|
77
|
+
converter.setOption('parseImgDimensions', true);
|
|
78
|
+
|
|
79
|
+
document.getElementById('submit').addEventListener('click', function(e) {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
|
|
82
|
+
const form = document.querySelector('form');
|
|
83
|
+
const description_raw = form.querySelector('textarea[name="content"]').value;
|
|
84
|
+
|
|
85
|
+
const description_html = converter.makeHtml(description_raw);
|
|
86
|
+
const title = form.querySelector('input[name="title"]').value;
|
|
87
|
+
const css = form.querySelector('textarea[name="css"]').value;
|
|
88
|
+
const theme_sel = form.querySelector('select[name="theme"]');
|
|
89
|
+
var theme = theme_sel.options[theme_sel.selectedIndex].value;
|
|
90
|
+
|
|
91
|
+
const tempForm = document.createElement('form');
|
|
92
|
+
tempForm.method = 'POST';
|
|
93
|
+
tempForm.action = '/dashboard/edit/{{ blog.name }}';
|
|
94
|
+
const date = new Date();
|
|
95
|
+
const inputs = {
|
|
96
|
+
title: title,
|
|
97
|
+
description_raw: description_raw,
|
|
98
|
+
description_html: description_html,
|
|
99
|
+
css: css,
|
|
100
|
+
theme: theme
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (let key in inputs) {
|
|
104
|
+
const input = document.createElement('input');
|
|
105
|
+
input.type = 'hidden';
|
|
106
|
+
input.name = key;
|
|
107
|
+
input.value = inputs[key];
|
|
108
|
+
tempForm.appendChild(input);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
document.body.appendChild(tempForm);
|
|
112
|
+
tempForm.submit();
|
|
113
|
+
document.body.removeChild(tempForm);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const editor = document.querySelector('textarea[name="content"]');
|
|
117
|
+
const uploadLink = document.getElementById("uploadLink");
|
|
118
|
+
const fileInput = document.getElementById("fileInput");
|
|
119
|
+
|
|
120
|
+
uploadLink.addEventListener("click", function(e) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
fileInput.click();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
fileInput.addEventListener("change", async () => {
|
|
126
|
+
const file = fileInput.files[0];
|
|
127
|
+
if (!file) return;
|
|
128
|
+
|
|
129
|
+
const formData = new FormData();
|
|
130
|
+
formData.append("file", file);
|
|
131
|
+
const cursorPos = editor.selectionStart;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch("/upload_image", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: formData
|
|
137
|
+
});
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
if (!res.ok) throw new Error(data.error || "Upload failed");
|
|
140
|
+
|
|
141
|
+
const markdown = ``;
|
|
142
|
+
|
|
143
|
+
const text = editor.value;
|
|
144
|
+
editor.value = text.slice(0, cursorPos) + markdown + text.slice(cursorPos);
|
|
145
|
+
editor.selectionStart = editor.selectionEnd = cursorPos + markdown.length;
|
|
146
|
+
editor.focus();
|
|
147
|
+
|
|
148
|
+
} catch (err) {
|
|
149
|
+
alert("Upload failed: " + err.message);
|
|
150
|
+
} finally {
|
|
151
|
+
fileInput.value = "";
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
document.getElementById('preview').addEventListener('click', function(e) {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
|
|
158
|
+
const form = document.querySelector('form');
|
|
159
|
+
const title = form.querySelector('input[name="title"]').value;
|
|
160
|
+
const content_raw = form.querySelector('textarea[name="content"]').value;
|
|
161
|
+
const theme_sel = form.querySelector('select[name="theme"]');
|
|
162
|
+
var theme = theme_sel.options[theme_sel.selectedIndex].value;
|
|
163
|
+
|
|
164
|
+
const content = converter.makeHtml(content_raw);
|
|
165
|
+
|
|
166
|
+
const tempForm = document.createElement('form');
|
|
167
|
+
tempForm.method = 'POST';
|
|
168
|
+
tempForm.action = '/dashboard/blog_preview';
|
|
169
|
+
tempForm.target = '_blank';
|
|
170
|
+
const date = getUTCDate();
|
|
171
|
+
const inputs = {
|
|
172
|
+
title: title,
|
|
173
|
+
content: content,
|
|
174
|
+
theme: theme
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
for (let key in inputs) {
|
|
178
|
+
const input = document.createElement('input');
|
|
179
|
+
input.type = 'hidden';
|
|
180
|
+
input.name = key;
|
|
181
|
+
input.value = inputs[key];
|
|
182
|
+
tempForm.appendChild(input);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
document.body.appendChild(tempForm);
|
|
186
|
+
tempForm.submit();
|
|
187
|
+
document.body.removeChild(tempForm);
|
|
188
|
+
});
|
|
189
|
+
</script>
|
|
190
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
</main>
|
|
2
|
+
<footer class="footer">
|
|
3
|
+
<a href="https://github.com/openwriteio/openwrite/">Github</a>
|
|
4
|
+
<a href="https://hub.docker.com/r/openwrite/openwrite">Dockerhub</a>
|
|
5
|
+
<a href="https://ko-fi.com/openwrite">{{ g.trans["buy_coffee"] }}</a>
|
|
6
|
+
<a href="https://main.openwrite.io/contact">{{ g.trans['contact'] }}</a>
|
|
7
|
+
<p>{{ g.trans["language"] }}: </p>
|
|
8
|
+
<select name="lang" onchange="location = '/set-lang/' + this.value">
|
|
9
|
+
{% for code in available_languages.keys() %}
|
|
10
|
+
<option value="{{ code }}" {% if code == current_lang %} selected {% endif %}>{{ available_languages[code].flag }} {{ available_languages[code].name }}</option>
|
|
11
|
+
{% endfor %}
|
|
12
|
+
</select>
|
|
13
|
+
</footer>
|
|
14
|
+
</div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>openwrite</title>
|
|
7
|
+
<meta name="description" content="Minimalist, opensource blog. Write, freely.">
|
|
8
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
|
9
|
+
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
10
|
+
{% if 'dashboard/stats' in request.path %}
|
|
11
|
+
<script src="{{ url_for('static', filename='script/chart.js') }}"></script>
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% if 'dashboard/edit' in request.path %}
|
|
14
|
+
<script src="{{ url_for('static', filename='script/showdown.min.js') }}"></script>
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% if 'dashboard/post' in request.path %}
|
|
17
|
+
<script src="{{ url_for('static', filename='script/showdown.min.js') }}"></script>
|
|
18
|
+
{% endif %}
|
|
19
|
+
{% if 'dashboard/import' in request.path %}
|
|
20
|
+
<script src="{{ url_for('static', filename='script/papaparse.min.js') }}"></script>
|
|
21
|
+
{% endif %}
|
|
22
|
+
<script src="{{ url_for('static', filename='script/openwrite.js') }}"></script>
|
|
23
|
+
{% block extra_scripts %}{% endblock %}
|
|
24
|
+
{% if g.captcha %}
|
|
25
|
+
<script type="module" src="{{ url_for('static', filename='script/site.min.js') }}" async defer></script>
|
|
26
|
+
<script nomodule src="{{ url_for('static', filename='script/site.compat.min.js') }}" async defer></script>
|
|
27
|
+
{% endif %}
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="container">
|
|
31
|
+
<header class="header">
|
|
32
|
+
{% for s in g.settings %}
|
|
33
|
+
{% if s.name == "logo" %}
|
|
34
|
+
<a href="/"><div style="background-image: url('{{ s.value }}');" id="logo"> </div></a>
|
|
35
|
+
{% endif %}
|
|
36
|
+
{% endfor %}
|
|
37
|
+
<div id="menu">
|
|
38
|
+
{% if g.mode == "multi" %}
|
|
39
|
+
{% if g.user %}
|
|
40
|
+
<a href="/dashboard">Dashboard</a>
|
|
41
|
+
{% endif %}
|
|
42
|
+
<a href="/discover">{{ g.trans['discover'] }}</a>
|
|
43
|
+
<a href="/instances">{{ g.trans["instances"] }}</a>
|
|
44
|
+
<a href="https://main.openwrite.io/hosting">{{ g.trans["hosting"] }}</a>
|
|
45
|
+
{% if g.user %}
|
|
46
|
+
<a href="/logout">{{ g.trans["logout"] }}</a>
|
|
47
|
+
{% else %}
|
|
48
|
+
{% if g.register_enabled %}
|
|
49
|
+
<a href="/register">{{ g.trans["register"] }}</a>
|
|
50
|
+
{% endif %}
|
|
51
|
+
<a href="/login">{{ g.trans["login"] }}</a>
|
|
52
|
+
{% endif %}
|
|
53
|
+
{% if g.isadmin == 1 %}
|
|
54
|
+
<a href="/admin">Admin</a>
|
|
55
|
+
{% endif %}
|
|
56
|
+
{% else %}
|
|
57
|
+
<a href="/dashboard">Dashboard</a>
|
|
58
|
+
{% endif %}
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|
|
61
|
+
<main class="content">
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
<article class="main-content">
|
|
3
|
+
<h1 class="centered">{{ g.trans['migrate_posts'] }}</h1>
|
|
4
|
+
<div class="message" style="display: none"></div>
|
|
5
|
+
<div class="error" style="display: none"></div>
|
|
6
|
+
<div style="display: flex; flex-direction: column; gap: 1rem">
|
|
7
|
+
<span class="form-field" style="margin-top: 2rem;">{{ g.trans['import_info'] | safe }}</span>
|
|
8
|
+
<input class="file_upload" type="file" name="posts_file">
|
|
9
|
+
<form method="post" action="/dashboard/import" style="display:none">
|
|
10
|
+
<h2 class="form-field" style="padding: 1rem;">{{ g.trans['select_posts'] }}</h2>
|
|
11
|
+
<span class="form-field" id="posts">
|
|
12
|
+
</span>
|
|
13
|
+
<div class="horizontal-fields" style="display: flex; gap: 1.5rem;">
|
|
14
|
+
<p style="line-height: 3rem;">{{ g.trans['select_blog_to_import'] }}</p>
|
|
15
|
+
<select id="blogs" style="min-width: 14rem; min-height: 3rem; color: #fff; background: #211932;">
|
|
16
|
+
{% for blog in blogs %}
|
|
17
|
+
<option value="{{ blog.name }}">{{ blog.name }}</option>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
</select>
|
|
20
|
+
</div>
|
|
21
|
+
<input type="submit" name="import" value="{{ g.trans['send'] }}" class="btn purple" style="margin-top: 2rem">
|
|
22
|
+
</form>
|
|
23
|
+
</div>
|
|
24
|
+
</article>
|
|
25
|
+
<script>
|
|
26
|
+
const file_input = document.querySelector("input[type='file']");
|
|
27
|
+
const form_input = document.querySelector("input[type='submit']");
|
|
28
|
+
let format = '';
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
form_input.addEventListener("click", function(e) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
|
|
34
|
+
const inputs = document.querySelectorAll("input[type='checkbox']");
|
|
35
|
+
const posts = [];
|
|
36
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
37
|
+
if (inputs[i].checked === true) {
|
|
38
|
+
const title = inputs[i].parentElement.children[1].textContent;
|
|
39
|
+
const content = document.querySelector("input[name='" + i + "_content']").attributes.content.value;
|
|
40
|
+
const date = document.querySelector("input[name='" + i + "_date']").attributes.content.value;
|
|
41
|
+
posts.push({ "title": title, "content": content, "date": date});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var e = document.getElementById("blogs");
|
|
46
|
+
var blog_name = e.options[e.selectedIndex].text;
|
|
47
|
+
json_data = {"blog": blog_name, "posts": posts};
|
|
48
|
+
|
|
49
|
+
fetch("/dashboard/import", {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json'
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(json_data)
|
|
55
|
+
}).then(response => {
|
|
56
|
+
if (response.ok) {
|
|
57
|
+
const div_msg = document.getElementsByClassName("message")[0];
|
|
58
|
+
div_msg.style.display = "block";
|
|
59
|
+
div_msg.innerHTML = '{{ g.trans['import_success1'] }} <a class="link" style="color: #dd98ff" href="/dashboard/edit/'+ blog_name +'">{{ g.trans['import_success2'] }}</a> {{ g.trans['import_success3'] }}';
|
|
60
|
+
} else {
|
|
61
|
+
const div_err = document.getElementsByClassName("error")[0];
|
|
62
|
+
div_err.style.display = "block";
|
|
63
|
+
div_err.innerHTML = '{{ g.trans['import_failed'] }}';
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
file_input.addEventListener("change", handle_files, false);
|
|
69
|
+
|
|
70
|
+
function handle_files() {
|
|
71
|
+
const posts_file = this.files[0];
|
|
72
|
+
let reader = new FileReader();
|
|
73
|
+
|
|
74
|
+
reader.addEventListener("load", () => {
|
|
75
|
+
if (posts_file.type == "text/xml") {
|
|
76
|
+
posts = read_xml(reader.result);
|
|
77
|
+
} else if(posts_file.type == "text/csv") {
|
|
78
|
+
posts = read_csv(reader.result);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
show_posts(posts);
|
|
82
|
+
}, false);
|
|
83
|
+
|
|
84
|
+
if (posts_file) {
|
|
85
|
+
const name = posts_file.name ? posts_file.name : "nope";
|
|
86
|
+
const type = posts_file.type ? posts_file.type : "nope";
|
|
87
|
+
if (type == "text/xml" || type == "text/csv") {
|
|
88
|
+
reader.readAsText(posts_file);
|
|
89
|
+
} else {
|
|
90
|
+
alert("filetype " + type + " not supported!");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function read_xml(content) {
|
|
96
|
+
const parser = new DOMParser();
|
|
97
|
+
const xmlDoc = parser.parseFromString(content, "text/xml");
|
|
98
|
+
const items = xmlDoc.getElementsByTagName("item");
|
|
99
|
+
const posts = [];
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < items.length; i++) {
|
|
102
|
+
const title = items[i].getElementsByTagName("title")[0].textContent.trim();
|
|
103
|
+
const content = items[i].getElementsByTagName("content:encoded")[0].textContent.trim();
|
|
104
|
+
const date_string = items[i].getElementsByTagName("pubDate")[0].textContent.trim();
|
|
105
|
+
const date = new Date(date_string).toUTCString();
|
|
106
|
+
posts.push({ title, content, date });
|
|
107
|
+
}
|
|
108
|
+
return posts;
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function read_csv(content) {
|
|
113
|
+
let posts = [];
|
|
114
|
+
Papa.parse(content, {
|
|
115
|
+
header: true,
|
|
116
|
+
complete: (results) => {
|
|
117
|
+
const res = results.data;
|
|
118
|
+
for (let i = 0; i < res.length; i++) {
|
|
119
|
+
if (res[i].title != undefined && res[i].content != undefined && res[i].date != undefined) {
|
|
120
|
+
const title = res[i].title;
|
|
121
|
+
const content = res[i].content;
|
|
122
|
+
const date_string = res[i].date;
|
|
123
|
+
const date = new Date(date_string).toUTCString();
|
|
124
|
+
posts.push({ title, content, date });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return posts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function show_posts(posts) {
|
|
133
|
+
document.querySelector("form").style.display = "block";
|
|
134
|
+
const posts_el = document.querySelector("#posts");
|
|
135
|
+
posts_el.innerHTML = '';
|
|
136
|
+
for (let i = 0; i < posts.length; i++) {
|
|
137
|
+
posts_el.innerHTML += '<div class="horizontal-fields" style="min-height: 30px; margin-bottom: 1.5rem; display: flex; gap: 1rem; text-align: center;"><input type="checkbox" name="' + i + '" id="' + i + '"><label style="margin-top: 6px;"" for="' + i + '">' + escapeHtml(posts[i].title) + '</label><input type="hidden" name="' + i + '_content" content="' + escapeHtml(posts[i].content) + '"><input type="hidden" name="' + i + '_date" content="' + posts[i].date + '"></div>';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
</script>
|
|
142
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
<h1 class="centered">{{ g.trans['instances'] }}</h1>
|
|
3
|
+
{{ g.trans['instances_text'] }}
|
|
4
|
+
<table class="table" style="margin-top: 1.5rem;">
|
|
5
|
+
<thead>
|
|
6
|
+
<th>{{ g.trans['name'] }}</th><th>URL</th><th>{{ g.trans['blogs_running'] }}</th><th>{{ g.trans['users'] }}</th><th>{{ g.trans['register'] }}</th><th>{{ g.trans['media_upload'] }}</th><th>Uptime</th>
|
|
7
|
+
</thead>
|
|
8
|
+
<tbody>
|
|
9
|
+
{% for i in instances %}
|
|
10
|
+
<td>{{ i.name }}</td><td><a href="{{ i.url }}" class="action">{{ i.url }}</a></td><td>{{ i.blogs }}</td><td>{{ i.users }}</td><td>{{ g.trans[i.register] }}</td><td>{{ g.trans[i.media] }}</td><td>{{ i.uptime }}</td>
|
|
11
|
+
{% endfor %}
|
|
12
|
+
</tbody>
|
|
13
|
+
</table>
|
|
14
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
|
|
3
|
+
<h1 class="centered">Login</h1>
|
|
4
|
+
|
|
5
|
+
<article class="main-content">
|
|
6
|
+
{% if error %}
|
|
7
|
+
<div class="error">{{ error }}</div>
|
|
8
|
+
{% endif %}
|
|
9
|
+
<form method="post" action="/login">
|
|
10
|
+
<span class="form-field">{{ g.trans["username"] }}:</span>
|
|
11
|
+
<input type="text" name="username">
|
|
12
|
+
<span class="form-field">{{ g.trans["password"] }}:</span>
|
|
13
|
+
<input type="password" name="password">
|
|
14
|
+
<input type="submit" name="sub" value="Login" class="btn purple" style="margin-top: 1rem;">
|
|
15
|
+
</form>
|
|
16
|
+
</article>
|
|
17
|
+
|
|
18
|
+
{% include 'footer.html' %}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
{% if error %}
|
|
3
|
+
<div class="error">{{ error }}</div>
|
|
4
|
+
{% endif %}
|
|
5
|
+
<div><a href="/dashboard" class="action">Dashboard</a> > <a href="/dashboard/edit/{{ blog.name }}" class="action">{{ blog.title }}</a> > {% if post %}{{ post.title }}{% else %}{{ g.trans['new_post'] }}{% endif %}</div>
|
|
6
|
+
<div class="container-row-fluid">
|
|
7
|
+
<h3>{% if post %}{{ g.trans['editing_post'] }}: {{ post.title }}{% else %}{{ g.trans['new_post_on'] }}: {{ blog.name }}{% endif %}</h3>
|
|
8
|
+
<a target="_blank" href="/dashboard/preview" class="btn purple" id="preview">{{ g.trans['preview'] }}</a>
|
|
9
|
+
</div>
|
|
10
|
+
<article class="main-content">
|
|
11
|
+
<form method="post" action="{% if post %}/dashboard/edit/{{ blog.name }}/{{ post.link }}{% else %}/dashboard/post/{{ blog.name }}{% endif %}">
|
|
12
|
+
<input type="text" name="title" placeholder="{{ g.trans['title'] }}" style="max-width: 100%; height: 50px; font-size: 25px; margin-bottom: 1rem;" value="{% if post %}{{ post.title }}{% endif %}">
|
|
13
|
+
<div style="display: flex; flex-direction: row; gap: 5px; align-items: center;">
|
|
14
|
+
<a href="#" class="small btn empty" data-action="h1">H1</a>
|
|
15
|
+
<a href="#" class="small btn empty" data-action="h2">H2</a>
|
|
16
|
+
<a href="#" class="small btn empty" data-action="h3">H3</a>
|
|
17
|
+
<a href="#" class="small btn empty" data-action="bold"><b>B</b></a>
|
|
18
|
+
<a href="#" class="small btn empty" data-action="italic"><i>I</i></a>
|
|
19
|
+
<a href="#" class="small btn empty" data-action="underline"><u>U</u></a>
|
|
20
|
+
<a href="#" class="small btn empty" data-action="strikethrough"><del>C</del></a>
|
|
21
|
+
<a href="#" class="small btn empty" data-action="quote">Q</a>
|
|
22
|
+
<a href="https://main.openwrite.io/markdown-syntax" style="text-decoration: none; color: #eeeeee; margin-left: auto" target="_blank">Markdown syntax</a>
|
|
23
|
+
</div>
|
|
24
|
+
<textarea autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" name="content" class="post_content" placeholder="{{ g.trans['content'] }}">{% if post %}{{ post.content_raw }}{% endif %}</textarea>
|
|
25
|
+
<div style="display: flex; justify-content: space-between">
|
|
26
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
27
|
+
<input type="checkbox" id="author" {% if post %} {% if post.author == "1" %}checked="true"{% endif %} {% else %} checked="true" {% endif %}>
|
|
28
|
+
<label for="author" style="text-align: left">{{ g.trans['include_author'] }}?</label>
|
|
29
|
+
</div>
|
|
30
|
+
{% if g.upload_enabled %}
|
|
31
|
+
<a href="#" id="uploadLink" class="btn empty">📎 {{ g.trans['upload_image'] }}</a>
|
|
32
|
+
<input type="file" id="fileInput" style="display: none;">
|
|
33
|
+
{% endif %}
|
|
34
|
+
</div>
|
|
35
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
36
|
+
<input type="checkbox" id="feed" {% if post %} {% if post.feed == "1" %}checked="true"{% endif %} {% else %} checked="true" {% endif %}>
|
|
37
|
+
<label for="feed" style="text-align: left">{{ g.trans['include_in_feed'] }}?</label>
|
|
38
|
+
</div>
|
|
39
|
+
<input type="submit" value="{% if post %}{{ g.trans['save'] }}{% else %}{{ g.trans['create'] }}{% endif %}" class="btn purple" id="submit">
|
|
40
|
+
</form>
|
|
41
|
+
</article>
|
|
42
|
+
<script nonce="{{ g.nonce }}">
|
|
43
|
+
var converter = new showdown.Converter();
|
|
44
|
+
converter.setOption('emoji', true);
|
|
45
|
+
converter.setOption('noHeaderId', true);
|
|
46
|
+
converter.setOption('openLinksInNewWindow', true);
|
|
47
|
+
converter.setOption('tables', true);
|
|
48
|
+
converter.setOption('tasklists', true);
|
|
49
|
+
converter.setOption('strikethrough', true);
|
|
50
|
+
|
|
51
|
+
document.getElementById('preview').addEventListener('click', function(e) {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
|
|
54
|
+
const form = document.querySelector('form');
|
|
55
|
+
const title = form.querySelector('input[name="title"]').value;
|
|
56
|
+
const content_raw = form.querySelector('textarea[name="content"]').value;
|
|
57
|
+
const author = form.querySelector("#author").checked;
|
|
58
|
+
|
|
59
|
+
const content = converter.makeHtml(content_raw);
|
|
60
|
+
|
|
61
|
+
const tempForm = document.createElement('form');
|
|
62
|
+
tempForm.method = 'POST';
|
|
63
|
+
tempForm.action = '/dashboard/preview';
|
|
64
|
+
tempForm.target = '_blank';
|
|
65
|
+
const date = getUTCDate();
|
|
66
|
+
const inputs = {
|
|
67
|
+
title: title,
|
|
68
|
+
content: content,
|
|
69
|
+
author: author ? '1' : '0',
|
|
70
|
+
blog_name: "{{ blog.name }}",
|
|
71
|
+
blog_title: "{{ blog.title }}",
|
|
72
|
+
theme: "{{ blog.theme }}",
|
|
73
|
+
date: date
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (let key in inputs) {
|
|
77
|
+
const input = document.createElement('input');
|
|
78
|
+
input.type = 'hidden';
|
|
79
|
+
input.name = key;
|
|
80
|
+
input.value = inputs[key];
|
|
81
|
+
tempForm.appendChild(input);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
document.body.appendChild(tempForm);
|
|
85
|
+
tempForm.submit();
|
|
86
|
+
document.body.removeChild(tempForm);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
document.getElementById('submit').addEventListener('click', function(e) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
|
|
92
|
+
const form = document.querySelector('form');
|
|
93
|
+
const title = form.querySelector('input[name="title"]').value;
|
|
94
|
+
const content_raw = form.querySelector('textarea[name="content"]').value;
|
|
95
|
+
const author = form.querySelector("#author").checked;
|
|
96
|
+
const feed = form.querySelector("#feed").checked;
|
|
97
|
+
|
|
98
|
+
const content = converter.makeHtml(content_raw);
|
|
99
|
+
|
|
100
|
+
const tempForm = document.createElement('form');
|
|
101
|
+
tempForm.method = 'POST';
|
|
102
|
+
tempForm.action = '{% if post %}/dashboard/edit/{{ blog.name }}/{{ post.link }}{% else %}/dashboard/post/{{ blog.name }}{% endif %}';
|
|
103
|
+
const date = new Date();
|
|
104
|
+
const inputs = {
|
|
105
|
+
title: title,
|
|
106
|
+
content_raw: content_raw,
|
|
107
|
+
content: content,
|
|
108
|
+
author: author ? '1' : '0',
|
|
109
|
+
feed: feed ? '1' : '0'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (let key in inputs) {
|
|
113
|
+
const input = document.createElement('input');
|
|
114
|
+
input.type = 'hidden';
|
|
115
|
+
input.name = key;
|
|
116
|
+
input.value = inputs[key];
|
|
117
|
+
tempForm.appendChild(input);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
document.body.appendChild(tempForm);
|
|
121
|
+
tempForm.submit();
|
|
122
|
+
document.body.removeChild(tempForm);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const editor = document.querySelector('textarea[name="content"]');
|
|
126
|
+
const uploadLink = document.getElementById("uploadLink");
|
|
127
|
+
const fileInput = document.getElementById("fileInput");
|
|
128
|
+
|
|
129
|
+
uploadLink.addEventListener("click", function(e) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
fileInput.click();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
fileInput.addEventListener("change", async () => {
|
|
135
|
+
const file = fileInput.files[0];
|
|
136
|
+
if (!file) return;
|
|
137
|
+
|
|
138
|
+
const formData = new FormData();
|
|
139
|
+
formData.append("file", file);
|
|
140
|
+
const cursorPos = editor.selectionStart;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch("/upload_image", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: formData
|
|
146
|
+
});
|
|
147
|
+
const data = await res.json();
|
|
148
|
+
if (!res.ok) throw new Error(data.error || "Upload failed");
|
|
149
|
+
|
|
150
|
+
const markdown = ``;
|
|
151
|
+
|
|
152
|
+
const text = editor.value;
|
|
153
|
+
editor.value = text.slice(0, cursorPos) + markdown + text.slice(cursorPos);
|
|
154
|
+
editor.selectionStart = editor.selectionEnd = cursorPos + markdown.length;
|
|
155
|
+
editor.focus();
|
|
156
|
+
|
|
157
|
+
} catch (err) {
|
|
158
|
+
alert("Upload failed: " + err.message);
|
|
159
|
+
} finally {
|
|
160
|
+
fileInput.value = "";
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
document.querySelectorAll('[data-action]').forEach(btn => {
|
|
165
|
+
btn.addEventListener('click', function (e) {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
const action = this.getAttribute('data-action');
|
|
168
|
+
|
|
169
|
+
const start = editor.selectionStart;
|
|
170
|
+
const end = editor.selectionEnd;
|
|
171
|
+
const selected = editor.value.slice(start, end);
|
|
172
|
+
|
|
173
|
+
let modified = selected;
|
|
174
|
+
switch (action) {
|
|
175
|
+
case "bold":
|
|
176
|
+
modified = `**${selected}**`;
|
|
177
|
+
break;
|
|
178
|
+
case "italic":
|
|
179
|
+
modified = `*${selected}*`;
|
|
180
|
+
break;
|
|
181
|
+
case "underline":
|
|
182
|
+
modified = `<u>${selected}</u>`;
|
|
183
|
+
break;
|
|
184
|
+
case "strikethrough":
|
|
185
|
+
modified = `~~${selected}~~`;
|
|
186
|
+
break;
|
|
187
|
+
case "quote":
|
|
188
|
+
modified = selected.split("\n").map(line => `> ${line}`).join("\n");
|
|
189
|
+
break;
|
|
190
|
+
case "h1":
|
|
191
|
+
modified = `# ${selected}`;
|
|
192
|
+
break;
|
|
193
|
+
case "h2":
|
|
194
|
+
modified = `## ${selected}`;
|
|
195
|
+
break;
|
|
196
|
+
case "h3":
|
|
197
|
+
modified = `### ${selected}`;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
editor.setRangeText(modified, start, end, "end");
|
|
202
|
+
editor.focus();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
</script>
|
|
207
|
+
{% include 'footer.html' %}
|