Browse code
Rework ui, save feed xml
- Limit width of article text
- Make feed list / article list collapsible
- Fix issue with formatting of code blocks
- Add a function for serializing the feed to an xml file
Showing 5 changed files
... | ... |
@@ -8,7 +8,7 @@ |
8 | 8 |
</head> |
9 | 9 |
<body> |
10 | 10 |
<div id="container" v-cloak> |
11 |
- <div class="left"> |
|
11 |
+ <div :class="{left: true, collapsed: collapsed}"> |
|
12 | 12 |
<div class="feeds"> |
13 | 13 |
<span class="pull-time">{{pull_time}}</span> |
14 | 14 |
<!--<div v-for="url in feed_urls">{{url}}</div>--> |
... | ... |
@@ -22,10 +22,10 @@ |
22 | 22 |
</div> |
23 | 23 |
</div> |
24 | 24 |
|
25 |
- <div v-if="current_feed.title !== null && current_feed.items.length > 0" class="current_feed"> |
|
26 |
- <h2>{{current_feed.title}}</h2> |
|
27 |
- <h3>{{current_feed.link}}</h3> |
|
28 |
- <div>{{current_feed.description}}</div> |
|
25 |
+ <div v-if="current_feed.metadata.title !== null && current_feed.items.length > 0" class="current_feed"> |
|
26 |
+ <h2>{{current_feed.metadata.title}}</h2> |
|
27 |
+ <h3>{{current_feed.metadata.link}}</h3> |
|
28 |
+ <div>{{current_feed.metadata.description}}</div> |
|
29 | 29 |
<ul> |
30 | 30 |
<li v-for="item in current_feed.items | orderBy 'path' -1"> |
31 | 31 |
<a v-on:click="get_item(item.path)"> |
... | ... |
@@ -36,9 +36,11 @@ |
36 | 36 |
</div> |
37 | 37 |
</div> |
38 | 38 |
<div class="right"> |
39 |
- <div v-if="current_item.title !== null"> |
|
39 |
+ <button @click="toggleCollapse" class="collapse-toggle">(Un)Collapse</button> |
|
40 |
+ <div v-if="current_item.title !== null" class="article-container"> |
|
40 | 41 |
<h2> |
41 | 42 |
<a href="{{current_item.link}}">{{current_item.title}}</a> |
43 |
+ <button @click="like(current_item, current_feed)"><3</button> |
|
42 | 44 |
</h2> |
43 | 45 |
<h3>{{current_item.author}}</h3> |
44 | 46 |
<div>{{{ item_content }}}</div> |
... | ... |
@@ -6,6 +6,7 @@ root = new Vue({ |
6 | 6 |
feeds: { |
7 | 7 |
feeds: [] |
8 | 8 |
}, |
9 |
+ collapsed: false, |
|
9 | 10 |
|
10 | 11 |
current_feed: { |
11 | 12 |
metadata: { |
... | ... |
@@ -34,13 +35,20 @@ root = new Vue({ |
34 | 35 |
item_content() { |
35 | 36 |
let result = null; |
36 | 37 |
if (this.current_item.content !== null) { |
37 |
- result = DOMPurify.sanitize(this.current_item.content); |
|
38 |
+ result = DOMPurify.sanitize(this.current_item.content, { |
|
39 |
+ FORBID_TAG: ['style'], |
|
40 |
+ FORBID_ATTR: ['style'], |
|
41 |
+ }); |
|
38 | 42 |
} |
39 | 43 |
return result; |
40 | 44 |
}, |
41 | 45 |
}, |
42 | 46 |
|
43 | 47 |
methods: { |
48 |
+ toggleCollapse() { |
|
49 |
+ this.collapsed = !this.collapsed; |
|
50 |
+ }, |
|
51 |
+ |
|
44 | 52 |
sanitize(html) { |
45 | 53 |
return DOMPurify.sanitize(html, { |
46 | 54 |
FORBID_TAG: ['style'], |
... | ... |
@@ -71,9 +79,34 @@ root = new Vue({ |
71 | 79 |
Object.assign(root.current_feed, result)); |
72 | 80 |
}, |
73 | 81 |
|
82 |
+ like(item, feed) { |
|
83 |
+ fetch('<<< LIKE WEBHOOK >>>', { |
|
84 |
+ method: 'POST', |
|
85 |
+ body: JSON.stringify({ |
|
86 |
+ 'event': 'like-item', |
|
87 |
+ 'item': item.link, |
|
88 |
+ 'title': item.title, |
|
89 |
+ 'author': item.author, |
|
90 |
+ 'feed-title': feed.metadata.title, |
|
91 |
+ 'feed-link': feed.metadata.link, |
|
92 |
+ }), |
|
93 |
+ }); |
|
94 |
+ }, |
|
95 |
+ |
|
74 | 96 |
get_item(path) { |
75 | 97 |
window.fetch(this.current_feed.base_path + path).then((resp) => resp.json()) |
76 | 98 |
.then((data) => { |
99 |
+ fetch('<<< READ WEBHOOK >>>', { |
|
100 |
+ method: 'POST', |
|
101 |
+ body: JSON.stringify({ |
|
102 |
+ 'event': 'read-item', |
|
103 |
+ 'item': data.link, |
|
104 |
+ 'title': data.title, |
|
105 |
+ 'author': data.author, |
|
106 |
+ 'feed-title': this.current_feed.metadata.title, |
|
107 |
+ 'feed-link': this.current_feed.metadata.link, |
|
108 |
+ }), |
|
109 |
+ }); |
|
77 | 110 |
window.history.pushState({ |
78 | 111 |
'current_feed': root.current_feed, |
79 | 112 |
'current_item': data |
... | ... |
@@ -29,10 +29,7 @@ a { |
29 | 29 |
vertical-align: middle; |
30 | 30 |
text-decoration: none; |
31 | 31 |
cursor: pointer; |
32 |
- /*width: 100%;*/ |
|
33 | 32 |
margin-right: 1em; |
34 |
- /*text-shadow: 0em 0em 0.1em #000*/ |
|
35 |
- /*, 0em 0em 0.2em #888;*/ |
|
36 | 33 |
} |
37 | 34 |
|
38 | 35 |
a:hover { |
... | ... |
@@ -43,11 +40,11 @@ a:focus { |
43 | 40 |
text-decoration: underline; |
44 | 41 |
} |
45 | 42 |
|
46 |
-.right { |
|
43 |
+#container > .right { |
|
47 | 44 |
overflow-y: auto; |
48 | 45 |
} |
49 | 46 |
|
50 |
-.right { |
|
47 |
+#container > .right { |
|
51 | 48 |
width: 50vw; |
52 | 49 |
height: 100vh; |
53 | 50 |
box-sizing: border-box; |
... | ... |
@@ -57,14 +54,22 @@ a:focus { |
57 | 54 |
right: 0; |
58 | 55 |
font-size: 16px; |
59 | 56 |
line-height: 1.25; |
60 |
- /*text-shadow: 0 0 0.1em #333, 0 0 0.2em #cce5dd;*/ |
|
61 | 57 |
} |
62 | 58 |
|
63 | 59 |
h1, h2, h3, h4, h5, h6 { |
64 | 60 |
border-bottom: 3px #28b0b1 double; |
65 | 61 |
} |
66 | 62 |
|
67 |
-.left { |
|
63 |
+#container > *, #container > .left > * { |
|
64 |
+ transition: width 2s ease; |
|
65 |
+} |
|
66 |
+ |
|
67 |
+.article-container { |
|
68 |
+ width: 30em; |
|
69 |
+ margin: auto; |
|
70 |
+} |
|
71 |
+ |
|
72 |
+#container > .left { |
|
68 | 73 |
border-right: 4px #28b0b1 double; |
69 | 74 |
box-shadow: 0px 0px 1em #28b0b1; |
70 | 75 |
width: 50vw; |
... | ... |
@@ -77,8 +82,9 @@ h1, h2, h3, h4, h5, h6 { |
77 | 82 |
} |
78 | 83 |
|
79 | 84 |
|
80 |
-.right img { |
|
81 |
- max-width: 50vw; |
|
85 |
+#container > .right img { |
|
86 |
+ max-width: 95%; |
|
87 |
+ height: auto; |
|
82 | 88 |
display: block; |
83 | 89 |
margin: auto; |
84 | 90 |
} |
... | ... |
@@ -116,3 +122,42 @@ h1, h2, h3, h4, h5, h6 { |
116 | 122 |
padding-left: 2.5em; |
117 | 123 |
margin-bottom: 0.5em; |
118 | 124 |
} |
125 |
+ |
|
126 |
+.collapsed { |
|
127 |
+ width: 0vw !important; |
|
128 |
+ overflow: hidden; |
|
129 |
+} |
|
130 |
+ |
|
131 |
+.collapsed > * { |
|
132 |
+ width: 0vw !important; |
|
133 |
+ overflow: hidden; |
|
134 |
+} |
|
135 |
+ |
|
136 |
+.collapsed + * { |
|
137 |
+ width: 100vw !important; |
|
138 |
+ padding-left: 25vw; |
|
139 |
+ padding-right: 25vw; |
|
140 |
+} |
|
141 |
+ |
|
142 |
+.collapse-toggle { |
|
143 |
+ position: fixed; |
|
144 |
+} |
|
145 |
+ |
|
146 |
+ |
|
147 |
+.code, code, pre { |
|
148 |
+ white-space: pre; |
|
149 |
+ background: black; |
|
150 |
+ padding: 1em; |
|
151 |
+ border: medium ridge; |
|
152 |
+ font-family: monospace; |
|
153 |
+ font-size: 10px; |
|
154 |
+} |
|
155 |
+ |
|
156 |
+.code .code, code code, pre pre, |
|
157 |
+.code pre, code .code, pre code, |
|
158 |
+.code code, code pre, pre .code, |
|
159 |
+{ |
|
160 |
+ background: transparent; |
|
161 |
+ padding: 0; |
|
162 |
+ border: none; |
|
163 |
+} |
... | ... |
@@ -113,13 +113,22 @@ |
113 | 113 |
(invoke-restart restart))) |
114 | 114 |
|
115 | 115 |
|
116 |
+(defun save-feed (feed output-file &key (if-exists :supersede)) |
|
117 |
+ (with-output-to-file (s output-file :if-exists if-exists) |
|
118 |
+ (plump:serialize (alimenta:doc feed) s))) |
|
119 |
+ |
|
116 | 120 |
(defun pull-and-store-feeds (feeds pull-directory) |
117 | 121 |
(mapcar (lambda (feed-url) |
118 | 122 |
(with-simple-restart (skip-feed "Skip ~a" feed-url) |
119 |
- (let ((feed (with-retry ("Pull feed again.") |
|
120 |
- (log-pull t feed-url)))) |
|
121 |
- (store (coerce-feed-link feed-url feed) |
|
122 |
- pull-directory)))) |
|
123 |
+ (let* ((feed (with-retry ("Pull feed again.") |
|
124 |
+ (log-pull t feed-url))) |
|
125 |
+ (result (store (coerce-feed-link feed-url feed) |
|
126 |
+ pull-directory))) |
|
127 |
+ (prog1 result |
|
128 |
+ (format t "Serializing XML...") |
|
129 |
+ (save-feed feed |
|
130 |
+ (merge-pathnames "feed.xml" |
|
131 |
+ (cadr result))))))) |
|
123 | 132 |
feeds)) |
124 | 133 |
|
125 | 134 |
(defun feed-index (index-stream pull-time paths) |