Browse code
gallery: add simple image gallery
Ed Langley authored on 23/01/2019 19:09:19
Showing 2 changed files
Showing 2 changed files
10 | 12 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,247 @@ |
1 |
+(defpackage :fwoar.gallery |
|
2 |
+ (:use :cl :araneus :alexandria :serapeum) |
|
3 |
+ (:export )) |
|
4 |
+(in-package :fwoar.gallery) |
|
5 |
+ |
|
6 |
+(defvar *url*) |
|
7 |
+(defvar *gallery*) |
|
8 |
+ |
|
9 |
+(defmacro new (class &rest initializer-syms) |
|
10 |
+ `(make-instance ,class |
|
11 |
+ ,@(mapcan (serapeum:op (list (alexandria:make-keyword _1) _1)) |
|
12 |
+ initializer-syms))) |
|
13 |
+ |
|
14 |
+(defclass gallery () |
|
15 |
+ ((%images :initarg :images :initform () :reader images))) |
|
16 |
+ |
|
17 |
+(defun make-gallery (images) |
|
18 |
+ (new 'gallery images)) |
|
19 |
+ |
|
20 |
+(defclass page () |
|
21 |
+ ((%gallery :initarg :gallery :reader gallery) |
|
22 |
+ (%page :initarg :page :initform 1 :reader page))) |
|
23 |
+ |
|
24 |
+(defun make-page (gallery page) |
|
25 |
+ (new 'page gallery page)) |
|
26 |
+ |
|
27 |
+(defclass image () |
|
28 |
+ ((%url :initarg :url :initform (error "need url for image") :reader url))) |
|
29 |
+(defclass video () |
|
30 |
+ ((%url :initarg :url :initform (error "need url for video") :reader url))) |
|
31 |
+ |
|
32 |
+(defmethod print-object ((o image) s) |
|
33 |
+ (format s "#.(make-image \"~a\")" (url o))) |
|
34 |
+(defmethod print-object ((o video) s) |
|
35 |
+ (format s "#.(make-video \"~a\")" (url o))) |
|
36 |
+(defmethod print-object ((o gallery) s) |
|
37 |
+ (format s "#.(make-gallery (~a images))" (length (images o)))) |
|
38 |
+ |
|
39 |
+(defun make-image (url) |
|
40 |
+ (new 'image url)) |
|
41 |
+(defun make-video (url) |
|
42 |
+ (new 'video url)) |
|
43 |
+ |
|
44 |
+(defun page->gallery (dom base-url) |
|
45 |
+ (check-type dom plump:node) |
|
46 |
+ (make-gallery |
|
47 |
+ (coerce (lquery:$ |
|
48 |
+ (inline dom) |
|
49 |
+ "a" |
|
50 |
+ (attr "href") |
|
51 |
+ (filter (lambda (u) (search "jpg" u))) |
|
52 |
+ (filter (complement (lambda (u) (search "thumb" u)))) |
|
53 |
+ (map (lambda (u) |
|
54 |
+ (with-output-to-string (s) |
|
55 |
+ (puri:render-uri |
|
56 |
+ (puri:merge-uris u base-url) |
|
57 |
+ s)))) |
|
58 |
+ (map 'make-image)) |
|
59 |
+ 'list))) |
|
60 |
+ |
|
61 |
+ |
|
62 |
+(define-controller root (params gallery) |
|
63 |
+ (let ((page (make-page gallery |
|
64 |
+ (parse-integer |
|
65 |
+ (or (cdr (assoc "page" params |
|
66 |
+ :test #'equalp)) |
|
67 |
+ "1"))))) |
|
68 |
+ page)) |
|
69 |
+ |
|
70 |
+(defmacro hostname-case (uri &body cases) |
|
71 |
+ `(serapeum:string-case (puri:uri-host ,uri) |
|
72 |
+ ,@cases)) |
|
73 |
+ |
|
74 |
+(defun get-site (uri) |
|
75 |
+ (hostname-case uri |
|
76 |
+ ("gfycat.com" :gfycat) |
|
77 |
+ ("i.redd.it" :ireddit) |
|
78 |
+ ("v.redd.it" :vreddit))) |
|
79 |
+ |
|
80 |
+(defgeneric transform-url (site uri) |
|
81 |
+ (:method (_ uri) |
|
82 |
+ (declare (ignore _)) |
|
83 |
+ (make-image (puri:render-uri uri nil))) |
|
84 |
+ (:method ((site (eql :gfycat)) uri) |
|
85 |
+ (make-video |
|
86 |
+ (format nil "~a.mp4" |
|
87 |
+ (puri:render-uri |
|
88 |
+ (fw.lu:prog1-bind (uri (puri:copy-uri uri)) |
|
89 |
+ (setf (puri:uri-host uri) "gfycat.com")) |
|
90 |
+ nil)))) |
|
91 |
+ (:method ((site (eql :vreddit)) uri) |
|
92 |
+ (make-video (puri:render-uri uri nil)))) |
|
93 |
+ |
|
94 |
+(defun ensure-uri (uri) |
|
95 |
+ (etypecase uri |
|
96 |
+ (string (puri:parse-uri uri)) |
|
97 |
+ (puri:uri uri))) |
|
98 |
+ |
|
99 |
+(defun process-uri-list (uris) |
|
100 |
+ (loop for raw-uri in uris |
|
101 |
+ for uri = (ensure-uri raw-uri) |
|
102 |
+ for transformed = (transform-url (get-site uri) uri) |
|
103 |
+ when transformed |
|
104 |
+ collect transformed)) |
|
105 |
+ |
|
106 |
+(spinneret:deftag css (body attrs) |
|
107 |
+ `(:style :type "text/css" |
|
108 |
+ ,@attrs |
|
109 |
+ (:raw |
|
110 |
+ ,(format nil "~%") |
|
111 |
+ ,(apply 'lass:compile-and-write |
|
112 |
+ body) |
|
113 |
+ ))) |
|
114 |
+ |
|
115 |
+(defmethod view :around ((name (eql 'root)) (model page)) |
|
116 |
+ (spinneret:with-html-string |
|
117 |
+ (:html |
|
118 |
+ (:head |
|
119 |
+ (css |
|
120 |
+ (* |
|
121 |
+ :box-sizing border-box) |
|
122 |
+ (html |
|
123 |
+ :outline "thin solid black") |
|
124 |
+ ((:or body html div section) |
|
125 |
+ :margin 0 |
|
126 |
+ :padding 0) |
|
127 |
+ |
|
128 |
+ (.gallery |
|
129 |
+ :position relative |
|
130 |
+ :display flex |
|
131 |
+ :width 100vw |
|
132 |
+ :height (calc (- 100vh 2em)) |
|
133 |
+ :overflow-y scroll |
|
134 |
+ :background "#888" |
|
135 |
+ |
|
136 |
+ :flex-wrap wrap) |
|
137 |
+ ((.gallery > div) |
|
138 |
+ :text-align center |
|
139 |
+ :width 25% |
|
140 |
+ :max-height 25vh |
|
141 |
+ :overflow hidden) |
|
142 |
+ ((.gallery img) |
|
143 |
+ :width 98% |
|
144 |
+ :height 98% |
|
145 |
+ :object-fit contain) |
|
146 |
+ ((.gallery video) |
|
147 |
+ :width 98% |
|
148 |
+ :height 98% |
|
149 |
+ :object-fit contain) |
|
150 |
+ ((.gallery > div.expanded) |
|
151 |
+ :position fixed |
|
152 |
+ :top 0 |
|
153 |
+ :left 0 |
|
154 |
+ :max-height (calc (- 100vh 2em)) |
|
155 |
+ :height 100% |
|
156 |
+ :width 100% |
|
157 |
+ :background "#eee") |
|
158 |
+ ((.gallery > (:and div.expanded :before)) |
|
159 |
+ :content " " |
|
160 |
+ :display inline-block |
|
161 |
+ :vertical-align middle |
|
162 |
+ :height 100%) |
|
163 |
+ ((.gallery > div.expanded > img) |
|
164 |
+ :display inline-block |
|
165 |
+ :vertical-align middle |
|
166 |
+ :height (calc (- 100vh 2em))) |
|
167 |
+ ((.gallery > div.expanded > video) |
|
168 |
+ :display inline-block |
|
169 |
+ :vertical-align middle))) |
|
170 |
+ (:body |
|
171 |
+ (let ((*gallery* (make-gallery (subseq (images (gallery model)) |
|
172 |
+ (* 52 (1- (page model))) |
|
173 |
+ (min (* 52 (page model)) |
|
174 |
+ (length (images (gallery model)))))))) |
|
175 |
+ (call-next-method)) |
|
176 |
+ (when (<= (* 52 (page model)) |
|
177 |
+ (length (images (gallery model)))) |
|
178 |
+ (:a :href (format nil "/?page=~d" (1+ (page model))) |
|
179 |
+ :style "width: 100%; text-align: center; display: inline-block;" |
|
180 |
+ "next")) |
|
181 |
+ (:script |
|
182 |
+ (:raw |
|
183 |
+ (ps:ps |
|
184 |
+ (ps:chain #() for-each |
|
185 |
+ (call (ps:chain document |
|
186 |
+ (query-selector-all ".gallery > div")) |
|
187 |
+ (lambda (it) |
|
188 |
+ (ps:chain it (add-event-listener |
|
189 |
+ "click" |
|
190 |
+ (lambda () |
|
191 |
+ (ps:chain #() for-each |
|
192 |
+ (call (ps:chain document (query-selector-all ".expanded")) |
|
193 |
+ (lambda (other) |
|
194 |
+ (unless (eql other it) |
|
195 |
+ (ps:chain other class-list (remove "expanded")))))) |
|
196 |
+ (ps:chain it class-list (toggle "expanded"))))))))))))))) |
|
197 |
+ |
|
198 |
+(define-view root ((model page)) |
|
199 |
+ (spinneret:with-html |
|
200 |
+ (:section.gallery |
|
201 |
+ (loop for img in (images *gallery*) |
|
202 |
+ do (call-current-view img))))) |
|
203 |
+ |
|
204 |
+(define-view root ((model image)) |
|
205 |
+ (spinneret:with-html |
|
206 |
+ (:div.image |
|
207 |
+ (:img :src (url model))))) |
|
208 |
+ |
|
209 |
+(define-view root ((model video)) |
|
210 |
+ (spinneret:with-html |
|
211 |
+ (:div.image |
|
212 |
+ (:video :autoplay "autoplay" (:source :src (url model) :type "video/mp4"))))) |
|
213 |
+ |
|
214 |
+(defun initialize-app (app gallery) |
|
215 |
+ (defroutes app |
|
216 |
+ (("/") (as-route 'root :gallery gallery)))) |
|
217 |
+ |
|
218 |
+(defun main (url) |
|
219 |
+ (let* ((app (make-instance 'ningle:<app>)) |
|
220 |
+ (gallery (page->gallery (plump:parse |
|
221 |
+ (babel:octets-to-string (drakma:http-request url :force-binary t) |
|
222 |
+ :encoding :latin-1)) |
|
223 |
+ url))) |
|
224 |
+ (initialize-app app gallery) |
|
225 |
+ (clack:clackup app))) |
|
226 |
+ |
|
227 |
+(defun get-reddit-items (r) |
|
228 |
+ (process-uri-list |
|
229 |
+ (mapcar (lambda (i) |
|
230 |
+ (fw.lu:pick '("data" "url") i)) |
|
231 |
+ (fw.lu:pick '("data" "children") r)))) |
|
232 |
+ |
|
233 |
+(defun reddit-main (subreddits) |
|
234 |
+ (let* ((app (make-instance 'ningle:<app>)) |
|
235 |
+ (gallery (make-gallery |
|
236 |
+ (mapcan |
|
237 |
+ (lambda (subreddit) |
|
238 |
+ (get-reddit-items |
|
239 |
+ (yason:parse |
|
240 |
+ (babel:octets-to-string |
|
241 |
+ (drakma:http-request (format nil "https://reddit.com/r/~a.json" subreddit) |
|
242 |
+ :force-binary t) |
|
243 |
+ :encoding :latin-1)))) |
|
244 |
+ subreddits)))) |
|
245 |
+ (setf *gallery* gallery) |
|
246 |
+ (initialize-app app gallery) |
|
247 |
+ (clack:clackup app))) |