├── 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 | --------------------------------------------------------------------------------