├── VERSION
├── README.md
└── Flamingo.js
/VERSION:
--------------------------------------------------------------------------------
1 | 101
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flamingo
2 | **Dive into the world of art, in your iPhone.**
3 |
4 |
5 |

6 |

7 |

8 |

9 |

10 |

11 |

12 |

13 |
14 |
15 | ## KR Description
16 | 플라밍고와 함께, 아이폰에서도 예술의 세계로 뛰어드세요.
17 | 플라밍고는 [Artvee](https://artvee.com/)를 통해 멋진 고대 미술작품들을 보여줍니다. 원하는 아티스트를 설정하거나, 카테고리를 설정하실 수도 있습니다. 저작권이 소멸된 작품들이기에, 원하는 사진은 언제든지 저장이 가능합니다.
18 | 또한 오프라인 모드가 준비되어 있습니다! 저장해둔 작품들을 인터넷 연결이 어려운 환경에서도 언제든 즐겨보세요.
19 |
20 | - 본 위젯의 무단 재배포, 재공유 및 상업적 판매는 엄격히 금지됩니다.
21 | - 본 위젯은 JulioKim님의 자료를 일부 참고해 제작하였습니다.
22 | - 오프라인 모드의 경우, 위젯 설정에서 비활성화가 가능합니다.
23 | - 작품을 다운로드할 때는 Wi-Fi 환경을 준비하시는 것을 권장합니다. 위젯 특성 상 데이터가 과도하게 소모될 수 있습니다.
24 |
25 | ## EN Description
26 | Flamingo is an iOS Scriptable widget that shows great artworks. It is powered by [Artvee](https://artvee.com/), which provides public domain artworks.
27 | You can either set your favorite artist, or select category for your artwork. Artworks are automatically downloaded and ready to be shown in offline mode.
28 |
29 | - Unauthorized Redistribution is strictly prohibited by the developer.
30 | - You can turn off offline mode in widget setting, which is summoned by launching widget in Scriptable app.
31 | - Wi-Fi network is recommended for downloading artworks.
32 |
--------------------------------------------------------------------------------
/Flamingo.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: pink; icon-glyph: dove;
4 | // Flamingo Widget v1.1 - by UnvsDev
5 | // Dive into the world of art, in your iPhone.
6 | // Learn More: https://github.com/unvsDev/Flamingo
7 |
8 | let today = new Date()
9 | let fm = FileManager.iCloud()
10 | const fDir = fm.joinPath(fm.documentsDirectory(), "/Flamingo/")
11 | const fDir2 = fm.joinPath(fDir, "/artworks/")
12 | const localPath = fm.joinPath(fDir, "artwork.txt")
13 | const prefPath = fm.joinPath(fDir, "flamPref.txt")
14 | var prefData = {
15 | artist: "!weekly",
16 | local: 0,
17 | refresh: 1800,
18 | title: true,
19 | rtitle: false,
20 | load: 0
21 | }
22 |
23 | var artworkOrgPref = {
24 | author: [],
25 | name: [],
26 | url: [],
27 | image: []
28 | }
29 |
30 | var bnum = 101 // Do not edit this area
31 |
32 | if(!fm.fileExists(fDir)){ fm.createDirectory(fDir) }
33 | if(fm.fileExists(prefPath)){
34 | prefData = JSON.parse(fm.readString(prefPath))
35 | }
36 | if(!fm.fileExists(localPath)){
37 | fm.writeString(localPath, JSON.stringify(artworkOrgPref))
38 | fm.createDirectory(fDir2)
39 | }
40 |
41 | var artveeCollections = {
42 | "Advertising Lithographs" : "https://artvee.com/collection/advertising-lithographs/",
43 | "Fashion Lithographs" : "https://artvee.com/collection/fashion-lithographs/",
44 | "Popular American Songs Covers" : "https://artvee.com/collection/popular-american-songs-covers/",
45 | "Fairy Tale illustrations" : "https://artvee.com/collection/fairy-tale-illustrations-from-elizabeth-tylers-home-and-school-series/",
46 | "NASA's Visions of the Future" : "https://artvee.com/collection/nasas-visions-of-the-future/",
47 | "Dietmar Winkler's MIT Posters" : "https://artvee.com/collection/dietmar-winklers-mit-posters/",
48 | "Book Promo Posters" : "https://artvee.com/collection/book-promo-posters/"
49 | }
50 |
51 | var endMode = false
52 | if(config.runsInApp){
53 | const settings = new UITable()
54 | settings.showSeparators = true
55 |
56 | const info = new UITableRow()
57 | info.dismissOnSelect = false
58 | info.addText("Welcome to Flamingo", "Developed by unvsDev")
59 | settings.addRow(info)
60 |
61 | const selectArtist = new UITableRow()
62 | selectArtist.dismissOnSelect = false
63 | selectArtist.addText("Set Artwork Filter")
64 | settings.addRow(selectArtist)
65 | selectArtist.onSelect = async () => {
66 | let alert = new Alert()
67 | alert.title = "Choose Topic"
68 | alert.message = "What artwork do you want to show in your widget?"
69 | alert.addAction("Specific Artist")
70 | alert.addAction("Artvee's Weekly Pick")
71 | alert.addAction("Special Collections")
72 | alert.addCancelAction("Cancel")
73 |
74 | let response = await alert.present()
75 | if(response == 0) {
76 | let inAlert = new Alert()
77 | inAlert.title = "Type your Artist"
78 | inAlert.message = "Just type artist's name,\nlike \"Leonardo Da Vinci\"."
79 | inAlert.addTextField("Leonardo Da Vinci", "")
80 | inAlert.addAction("Done")
81 | inAlert.addCancelAction("Cancel")
82 |
83 | if(await inAlert.present() != -1){
84 | prefData.artist = inAlert.textFieldValue()
85 | }
86 | } else if(response == 1){
87 | prefData.artist = "!weekly"
88 | } else if(response == 2){
89 | const collectionView = new UITable()
90 | collectionView.showSeparators = true
91 |
92 | for(name in artveeCollections){
93 | const collectionRow = new UITableRow()
94 | collectionRow.dismissOnSelect = true
95 | collectionRow.addText(name)
96 | collectionView.addRow(collectionRow)
97 |
98 | collectionRow.onSelect = async () => {
99 | prefData.artist = artveeCollections[name]
100 | }
101 | }
102 |
103 | await collectionView.present()
104 | }
105 | }
106 |
107 | const selectLocal = new UITableRow()
108 | selectLocal.dismissOnSelect = false
109 | selectLocal.addText("Local Artworks")
110 | settings.addRow(selectLocal)
111 | selectLocal.onSelect = async () => {
112 | let alert = new Alert()
113 | alert.title = "Get Local Artworks?"
114 | alert.message = "Widget will load only downloaded Artworks, enable you to surf through offline."
115 | alert.addAction("Always download Artworks")
116 | alert.addAction("Show only local Artworks")
117 | alert.addDestructiveAction("Never download Artworks")
118 | alert.addCancelAction("Cancel")
119 |
120 | let response = await alert.present()
121 | if(response != -1){
122 | prefData.local = response
123 | }
124 | }
125 |
126 | const selectRef = new UITableRow()
127 | selectRef.dismissOnSelect = false
128 | selectRef.addText("Refresh Interval")
129 | settings.addRow(selectRef)
130 | selectRef.onSelect = async () => {
131 | let alert = new Alert()
132 | alert.title = "Refresh Interval?"
133 | alert.message = "Due to iOS Widget policy, refresh could be delayed up to several hours."
134 | alert.addTextField("(second)", prefData.refresh.toString())
135 | alert.addAction("Done")
136 | alert.addCancelAction("Cancel")
137 |
138 | let response = await alert.present()
139 | if(response != -1){
140 | prefData.refresh = parseInt(alert.textFieldValue())
141 | }
142 | }
143 |
144 | const selectTitle = new UITableRow()
145 | selectTitle.dismissOnSelect = false
146 | selectTitle.addText("Show Artwork's Detail")
147 | settings.addRow(selectTitle)
148 | selectTitle.onSelect = async () => {
149 | let alert = new Alert()
150 | alert.title = "Show Artwork's Detail?"
151 | alert.message = "Widget will show Artwork's Name and Author."
152 | alert.addAction("Yes")
153 | alert.addAction("No")
154 |
155 | let response = await alert.present()
156 | if(response != -1){
157 | prefData.title = response ? false : true
158 | }
159 | }
160 |
161 | const selectRt = new UITableRow()
162 | selectRt.dismissOnSelect = false
163 | selectRt.addText("Show Last Refreshed Time")
164 | settings.addRow(selectRt)
165 | selectRt.onSelect = async () => {
166 | let alert = new Alert()
167 | alert.title = "Show Last Refreshed Time?"
168 | alert.message = "Widget will show Artwork's last refreshed time."
169 | alert.addAction("Yes")
170 | alert.addAction("No")
171 |
172 | let response = await alert.present()
173 | if(response != -1){
174 | prefData.rtitle = response ? false : true
175 | }
176 | }
177 |
178 | const selectLoad = new UITableRow()
179 | selectLoad.dismissOnSelect = false
180 | selectLoad.addText("Artwork Search Range")
181 | settings.addRow(selectLoad)
182 | selectLoad.onSelect = async () => {
183 | let alert = new Alert()
184 | alert.title = "Input search range"
185 | alert.message = "Widget will search this amount of artworks at once. Larger range allows you to find various artworks, However smaller range lets you see artwork faster. Remember that if there's not enough artworks, this range can be ignored."
186 | alert.addAction("20 (Small)")
187 | alert.addAction("50 (Medium)")
188 | alert.addAction("100 (Big)")
189 | alert.addAction("200 (Large)")
190 | alert.addCancelAction("Cancel")
191 |
192 | let response = await alert.present()
193 | if(response != -1){
194 | prefData.load = response
195 | }
196 | }
197 |
198 | const resetOption = new UITableRow()
199 | resetOption.dismissOnSelect = true
200 | resetOption.addText("Reset all data")
201 | settings.addRow(resetOption)
202 | resetOption.onSelect = async () => {
203 | endMode = true
204 | let alert = new Alert()
205 | alert.title = "Reset Confirmation"
206 | alert.message = "Do you really want to reset all data? Since Thanos helps me to delete them, you cannot undo your action."
207 | alert.addDestructiveAction("Delete only user data")
208 | alert.addDestructiveAction("Delete all artworks with data")
209 | alert.addCancelAction("No")
210 |
211 | let response = await alert.present()
212 | if(response == 0){
213 | await fm.remove(prefPath)
214 | } else if(response == 1){
215 | await fm.remove(fDir)
216 | }
217 | }
218 |
219 | const saveOption = new UITableRow()
220 | saveOption.dismissOnSelect = true
221 | saveOption.addText("Save and quit")
222 | settings.addRow(saveOption)
223 | saveOption.onSelect = () => {
224 | endMode = true
225 | }
226 |
227 | await settings.present()
228 | fm.writeString(prefPath, JSON.stringify(prefData))
229 | }
230 |
231 | if(endMode){ return 0 }
232 |
233 | prefData = JSON.parse(fm.readString(prefPath))
234 |
235 | const artistInput = prefData.artist
236 | const artist = artistInput.replace(/ /gi, "-").toLowerCase()
237 |
238 | async function loadArts(artist){
239 | var chunk
240 | if(prefData.load == 0) { chunk = 20 }
241 | else if(prefData.load == 1) { chunk = 50 }
242 | else if(prefData.load == 2) { chunk = 100 }
243 | else { chunk = 200 }
244 |
245 | const baseUrl = 'https://artvee.com'
246 | var source
247 | if(artistInput == "!weekly") {
248 | source = 'https://artvee.com/highlights/'
249 | } else if(artistInput.indexOf("http") != -1){
250 | source = artistInput
251 | } else {
252 | source = `${baseUrl}/artist/${artist}/?per_page=`+ chunk
253 | }
254 |
255 | let webView = new WebView()
256 | await webView.loadURL(source)
257 |
258 | return webView.evaluateJavaScript(`
259 | let arts = [...document.querySelectorAll('.products .product-grid-item .product-wrapper')].map((ele) => {
260 | let productLinkEle = ele.querySelector('.product-element-top')
261 | let imageEle = productLinkEle.querySelector('img')
262 | let productInfoEle = ele.querySelector('.product-element-bottom')
263 | return {
264 | id: parseInt(productInfoEle.querySelector('.linko').dataset.id),
265 | title: productInfoEle.querySelector('h3.product-title > a').innerText,
266 | artist: {
267 | name: productInfoEle.querySelector('.woodmart-product-brands-links > a').innerText,
268 | info: productInfoEle.querySelector('.woodmart-product-brands-links').innerText,
269 | link: productInfoEle.querySelector('.woodmart-product-brands-links > a').getAttribute('href'),
270 | },
271 | link: productLinkEle.getAttribute('href'),
272 | image: {
273 | link: imageEle.getAttribute('src'),
274 | width: imageEle.getAttribute('width'),
275 | height: imageEle.getAttribute('height'),
276 | }
277 | }
278 | }).sort((prev, next) => prev.id - next.id)
279 |
280 | completion(arts)
281 | `, true)
282 | }
283 |
284 | var offlineMode = (prefData.local == 1) ? true : false
285 |
286 | let arts = []
287 | try{
288 | const uServer = "https://github.com/unvsDev/Flamingo/raw/main/VERSION"
289 | const cServer = "https://github.com/unvsDev/Flamingo/raw/main/Flamingo.js"
290 | var minVer = parseInt(await new Request(uServer).loadString())
291 | if(bnum < minVer){
292 | var code = await new Request(cServer).loadString()
293 | fm.writeString(fm.joinPath(fm.documentsDirectory(), Script.name() + ".js"), code)
294 | return 0
295 | }
296 | } catch(e) {
297 | offlineMode = true
298 | }
299 |
300 | if(!offlineMode){
301 | arts = await loadArts(artist)
302 | if(arts.length < 1){
303 | throw new Error("[!] No result found.")
304 | return 0
305 | }
306 | }
307 |
308 | let targetArt; let todayIdx
309 | if(offlineMode){
310 | let localData = JSON.parse(fm.readString(localPath))
311 | todayIdx = Math.floor(Math.random() * localData.image.length)
312 |
313 | var artAuthor = localData.author[todayIdx]
314 | var artName = localData.name[todayIdx]
315 | var artUrl = localData.url[todayIdx]
316 | targetArt = await fm.readImage(fm.joinPath(fDir2, localData.image[todayIdx] + ".jpg"))
317 | } else {
318 | // console.log('arts: ' + JSON.stringify(arts, null, 4))
319 | todayIdx = Math.floor(Math.random() * arts.length)
320 | let todayArt = arts[todayIdx]
321 |
322 | var artId = todayArt.id
323 | var artAuthor = todayArt.artist.info.split("(")[0]
324 | var artName = todayArt.title.split("(")[0]
325 | var artUrl = todayArt.link
326 |
327 | let localData = JSON.parse(fm.readString(localPath))
328 | if(localData.image.indexOf(artId) != -1){
329 | targetArt = await fm.readImage(fm.joinPath(fDir2, artId + ".jpg"))
330 | console.log("[*] Getting preloaded image.. (" + artId + ")")
331 | } else {
332 | targetArt = await new Request(todayArt.image.link).loadImage()
333 | console.log("[*] Downloaded image.. (" + artId + ")")
334 | }
335 |
336 | if(prefData.local == 0){
337 | let localData = JSON.parse(fm.readString(localPath))
338 | if(localData.image.indexOf(artId) == -1){
339 | localData.author.push(artAuthor)
340 | localData.name.push(artName)
341 | localData.image.push(artId)
342 | localData.url.push(artUrl)
343 | fm.writeImage(fm.joinPath(fDir2, artId + ".jpg"), targetArt)
344 | fm.writeString(localPath, JSON.stringify(localData))
345 | }
346 | }
347 | }
348 |
349 | let widget = new ListWidget()
350 | widget.refreshAfterDate = new Date(Date.now() + 1000 * prefData.refresh)
351 | widget.url = artUrl
352 |
353 | widget.addSpacer()
354 |
355 | let hStack = widget.addStack()
356 | hStack.layoutHorizontally()
357 |
358 | let lStack = hStack.addStack()
359 | lStack.layoutVertically()
360 |
361 | lStack.addSpacer()
362 |
363 | if(prefData.title){
364 | let author = lStack.addText(artAuthor)
365 | author.textColor = Color.white()
366 | author.font = Font.lightMonospacedSystemFont(12)
367 |
368 | let title = lStack.addText(artName)
369 | title.textColor = Color.white()
370 | title.font = Font.boldMonospacedSystemFont(15)
371 | }
372 |
373 | if(prefData.rtitle){
374 | let rTitle = lStack.addText("Last updated: " + formatTime(today) + " (" + `${todayIdx + 1} / ${arts.length}` + ", ID: " + artId + ")")
375 | rTitle.textColor = Color.white()
376 | rTitle.font = Font.lightMonospacedSystemFont(9)
377 | }
378 |
379 | function formatTime(date) {
380 | let df = new DateFormatter()
381 | df.useNoDateStyle()
382 | df.useShortTimeStyle()
383 | return df.string(date)
384 | }
385 |
386 | hStack.addSpacer()
387 |
388 | let rStack = hStack.addStack()
389 | rStack.layoutVertically()
390 |
391 | rStack.addSpacer()
392 |
393 | let logo = rStack.addText("FLAMINGO")
394 | logo.textColor = Color.white()
395 | logo.font = Font.blackMonospacedSystemFont(8)
396 |
397 | widget.addSpacer(3)
398 |
399 | widget.backgroundImage = targetArt
400 | widget.presentLarge()
401 |
--------------------------------------------------------------------------------