├── art ├── .gitkeep └── blank.md ├── meme └── .gitkeep ├── text └── .gitkeep ├── .gitignore ├── src ├── shell │ ├── computed.sh │ ├── art2text.sh.in │ ├── genartlist.sh.in │ └── imgcheck.py ├── .github │ └── workflows │ │ ├── check-similarity-img.yml │ │ ├── generate-article-page.yml │ │ └── generate-config-file.yml ├── static │ ├── style.css │ └── scripts │ │ └── index.js.in └── index.html.in ├── LICENSE ├── README_zh.md ├── Makefile └── README.md /art/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /meme/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /text/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /art/blank.md: -------------------------------------------------------------------------------- 1 | # Lorem ipsum 2 | 3 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum sequi itaque blanditiis repellendus aspernatur vitae modi? 4 | Eum adipisci unde quia perferendis aspernatur, dicta ullam officiis sapiente illum expedita perspiciatis ex! 5 | -------------------------------------------------------------------------------- /src/shell/computed.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | echo 'export default { 3 | items: [' > ./static/scripts/config.js 4 | 5 | find meme -type f -print0 | grep -z -i -E '\.(jpg|png|jfif|webp|gif|jpeg|bmp)$' | sort -z | while IFS= read -r -d '' file_name 6 | do 7 | echo -n ' ' >> ./static/scripts/config.js 8 | echo "$(jq -R -c . <<< "$file_name")," >> ./static/scripts/config.js 9 | # echo ',' >> ./static/scripts/config.js 10 | done 11 | 12 | echo ' ] 13 | }' >> ./static/scripts/config.js 14 | -------------------------------------------------------------------------------- /src/.github/workflows/check-similarity-img.yml: -------------------------------------------------------------------------------- 1 | name: Check Similarity Images 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | check-similarity-img: 8 | name: Check Similarity Images 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | ref: ${{ github.ref }} 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: 3.10.* 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install Pillow 23 | - name: check similarity images 24 | run: | 25 | python shell/imgcheck.py 26 | - uses: stefanzweifel/git-auto-commit-action@v4 27 | with: 28 | commit_message: 'chore: update similarity images data' 29 | file_pattern: static/data/similar_images.json 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/.github/workflows/generate-article-page.yml: -------------------------------------------------------------------------------- 1 | name: Generate article page 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - 'art/*' 9 | - 'shell/art2text.sh' 10 | - 'shell/genartlist.sh' 11 | - '.github/workflows/generate-article-page.yml' 12 | 13 | jobs: 14 | generate-article-page: 15 | name: Generate article page 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | ref: ${{ github.ref }} 23 | - name: remove previous build 24 | run: | 25 | rm -f ./text/*.html 26 | - name: generate directory 27 | run: | 28 | ./shell/genartlist.sh 29 | - name: build article 30 | run: | 31 | ./shell/art2text.sh 32 | - uses: stefanzweifel/git-auto-commit-action@v4 33 | with: 34 | commit_message: 'chore: update article page' 35 | file_pattern: 'text/*.html' 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /src/.github/workflows/generate-config-file.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Generate config file 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | paths: 11 | - 'meme/**' 12 | - '.github/workflows/generate-config-file.yml' 13 | 14 | jobs: 15 | generate-config-file: 16 | name: Generate config file 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | ref: ${{ github.ref }} 24 | - name: build config 25 | run: | 26 | ./shell/computed.sh 27 | cat static/scripts/config.js 28 | - uses: stefanzweifel/git-auto-commit-action@v4 29 | with: 30 | commit_message: 'chore: update config file' 31 | file_pattern: static/scripts/config.js 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NoneMeme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/static/style.css: -------------------------------------------------------------------------------- 1 | #view { 2 | text-align: center; 3 | } 4 | 5 | #view > div { 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | #view a { 11 | color: var(--h3-color); 12 | text-decoration: none; 13 | cursor: default; 14 | } 15 | 16 | #view img { 17 | max-height: 70vh; 18 | } 19 | 20 | #gallery { 21 | display: flex; 22 | align-items: flex-start; 23 | } 24 | 25 | .column { 26 | margin-right: 10px; 27 | width: 33%; 28 | } 29 | 30 | .item { 31 | margin: 10px 0; 32 | padding: 50px 20px; 33 | animation: 1s fadeInUp; 34 | } 35 | 36 | .item a { 37 | display: flex; 38 | flex-direction: column; 39 | align-items: stretch; 40 | } 41 | 42 | article.item > a > span { 43 | color: var(--h3-color); 44 | font-size: 15px; 45 | text-align: center; 46 | background: var(--muted-border-color); 47 | border-bottom-right-radius: 5px; 48 | border-bottom-left-radius: 5px; 49 | } 50 | 51 | footer { 52 | text-align: center; 53 | } 54 | 55 | @keyframes fadeInUp { 56 | 0% { 57 | opacity: 0; 58 | transform: translateY(50px); 59 | } 60 | 100% { 61 | opacity: 1; 62 | transform: translateY(0); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/shell/art2text.sh.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | direrr() { 4 | echo -e "\e[41m[ERROR] You should run me under \e[1;33mthe root of this repository.\e[0m" 5 | exit 1 6 | } 7 | 8 | [[ -d "art" ]] || direrr 9 | [[ -d "text" ]] || direrr 10 | 11 | for doc in art/*.md 12 | do 13 | echo Converting "'${doc:4}'" ... 14 | cat > "text/${doc:4:-3}.html" < 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | @TITLE@ | @DESC@ 25 | 26 | 27 | 32 | 33 | 34 | 35 |
36 |

@TITLE@ | @T_MEMETXT@

37 |
38 | 39 | EOF 40 | cat "$doc" >> "text/${doc:4:-3}.html" 41 | cat >> "text/${doc:4:-3}.html" < 43 |
44 | 45 |
46 |

@FOOTER@

47 |
48 |
49 | 50 | 51 | 52 | 53 | EOF 54 | done 55 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

MemeBox

2 | 3 |
NoneMeme项目使用的网站模板
4 | 5 | * * * 6 | 7 | ### 描述 8 | 9 | 本模板提取自 [NoneMeme/NoneMeme](https://github.com/NoneMeme/NoneMeme). 10 | 11 | Memebox 可以存放多种内容,如`图片`和`Markdown文档`。 12 | 13 | ### 构建 14 | 15 | 1. 准备一个`make`工具。 16 | 17 | 2. 使用本模板,生成你的仓库。 18 | 19 | 3. 克隆你的仓库。 20 | 21 | 4. 打开终端,进入你的仓库。 22 | 23 | 5. 运行 make。 24 | 25 | > 注意: 默认的变量 `PAGELANG` 现在是 `en` (英文). 26 | > 27 | > 详情: [自定义文字](#自定义文字). 28 | 29 | 6. 将你的资源放入 `art/` (文档) 或 `meme/` (图片). 30 | 31 | > 注意: Memebox **不** 含有网站图标。 32 | > 33 | > 你应该将你的图标放入`static/`目录内。 34 | > 35 | > 更多信息可查看`make`的输出内容。 36 | 37 | 7. 提交并推送。 38 | 39 | ### 自定义文字 40 | 41 | 通常,你可能想自定义网站的标题等等。 42 | 43 | 你可以通过在终端中定义以下环境变量值来进行修改操作: 44 | 45 | |名称|D描述| 46 | |:----|:----| 47 | |PAGELANG|.html文件的页面语言标记,同时也定义了网站使用的语言,例如:"zh"| 48 | |TITLE|网站标题, 例如:"MemeBox"| 49 | |DESC|网站描述,例如:"Joy for Everyone"| 50 | |TDESC|文字梗的描述| 51 | |FOOTER|每个页面的脚注| 52 | 53 | 比如: 54 | 55 | PAGELANG=en TITLE=Foo DESC=Bar FOOTER="Lorem ipsum" make 56 | 57 | 如果默认信息你看不顺眼,没关系,它们也是可以修改的: 58 | 59 | |名称|描述| 60 | |:----|:----| 61 | |T_MEMEPIC|跳转至图片页面的链接名称| 62 | |T_MEMETXT|跳转至文字页面的超链接名称| 63 | |T_DOWNLOAD|下载一张图片时弹出的提示| 64 | |T_ANOTHER|随机选择一张图片时弹出的提示| 65 | |T_BACK|返回主页的提示| 66 | |T_ZOOMIN|查看大图的提示| 67 | |T_NIMGS|图片统计信息| 68 | 69 | ### 许可证 70 | 71 | 本项目使用 [MIT 协议](LICENSE) 开源。 72 | 73 | ### 特别感谢 74 | 75 | - **[modcrafts/a60-shop](https://github.com/modcrafts/a60-shop)** 76 | 77 | - **[picocss/pico](https://github.com/picocss/pico/tree/f9e97c0bf430df8fa3f730eb6a6e84f63d4a9b0c)** 78 | 79 | - **[MarketingPipeline/Markdown-Tag](https://github.com/MarketingPipeline/Markdown-Tag)** 80 | 81 | - **[NoneMeme/NoneMeme](https://github.com/NoneMeme/NoneMeme)** 82 | -------------------------------------------------------------------------------- /src/shell/genartlist.sh.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | direrr() { 4 | echo -e "\e[41m[ERROR] You should run me under \e[1;33mthe root of this repository.\e[0m" 5 | exit 1 6 | } 7 | 8 | titleerr() { 9 | rm -f text/index.html 10 | echo -e "\e[41m[ERROR] The title of the article \e[1;33mcannot be found!\e[0m" 11 | exit 1 12 | } 13 | 14 | [[ -d "art" ]] || direrr 15 | [[ -d "text" ]] || direrr 16 | 17 | echo "Generating article directory..." 18 | 19 | cat > "text/index.html" < 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | @TITLE@ | @DESC@ 30 | 31 | 32 | 37 | 38 | 39 | 40 |
41 |

@TITLE@ | @T_MEMEPIC@

42 |
43 |
44 | 45 | @TDESC@ 46 | 47 | EOF 48 | 49 | for doc in art/*.md 50 | do 51 | echo Adding "'${doc:4:-3}'" ... 52 | title="$(grep -E '^# ' "$doc" | head -n1)" 53 | [[ -z "$title" ]] && titleerr 54 | echo "- [${title:2}](./${doc:4:-3}.html)" >> "text/index.html" 55 | done 56 | 57 | cat >> "text/index.html" < 59 |
60 | 61 |
62 |

@FOOTER@

63 |
64 |
65 | 66 | 67 | 68 | 69 | EOF 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # memebox generating 2 | 3 | PAGELANG?=en 4 | 5 | TITLE?=MemeBox 6 | DESC?=Joy for Everyone 7 | TDESC?=> _Text memes description._ 8 | FOOTER?=** Footer ** 9 | 10 | ifeq (${PAGELANG}, zh) 11 | T_MEMEPIC?=图片梗 12 | T_MEMETXT?=文字梗 13 | T_DOWNLOAD?=下载图片 14 | T_ANOTHER?=梗图不喜欢?换一张试试看 15 | T_BACK?=返回画廊 16 | T_ZOOMIN?=查看大图 17 | T_NIMGS?=目前已有 $${items.length} 张。 18 | endif 19 | 20 | T_MEMEPIC?=Picture memes 21 | T_MEMETXT?=Text memes 22 | T_DOWNLOAD?=Download 23 | T_ANOTHER?=Not prefer to it? Try another one 24 | T_BACK?=Back to gallery 25 | T_ZOOMIN?=Zoom in 26 | T_NIMGS?=currently including $${items.length} image(s). 27 | 28 | 29 | .PHONY: clean icon copyandstub fixshperm 30 | 31 | all: copyandstub shell/genartlist.sh shell/art2text.sh static/scripts/index.js index.html icon fixshperm 32 | 33 | index.html shell/genartlist.sh shell/art2text.sh static/scripts/index.js: %: src/%.in 34 | sed 's%@TITLE@%${TITLE}%g' $^ \ 35 | | sed 's%@DESC@%${DESC}%g' \ 36 | | sed 's%@TDESC@%${TDESC}%g' \ 37 | | sed 's%@FOOTER@%${FOOTER}%g' \ 38 | | sed 's%@PAGELANG@%${PAGELANG}%g' \ 39 | | sed 's%@T_MEMEPIC@%${T_MEMEPIC}%g' \ 40 | | sed 's%@T_MEMETXT@%${T_MEMETXT}%g' \ 41 | | sed 's%@T_DOWNLOAD@%${T_DOWNLOAD}%g' \ 42 | | sed 's%@T_ANOTHER@%${T_ANOTHER}%g' \ 43 | | sed 's%@T_BACK@%${T_BACK}%g' \ 44 | | sed 's%@T_ZOOMIN@%${T_ZOOMIN}%g' \ 45 | | sed 's%@T_NIMGS@%${T_NIMGS}%g' > $@ 46 | 47 | fixshperm: shell/genartlist.sh shell/art2text.sh 48 | chmod +x $^ 49 | 50 | icon: 51 | @echo 52 | @echo "*** Two icon files are used:" 53 | @echo "*** - static/favicon.ico" 54 | @echo "*** - static/favicon.png" 55 | @echo "*** Please put your icons to the right place." 56 | 57 | copyandstub: 58 | mkdir -pv shell static/data/images static/scripts 59 | touch static/data/.gitkeep static/data/images/.gitkeep 60 | cp -rf src/.github . 61 | cp -rf src/static/style.css static/ 62 | cp -rf src/shell/computed.sh src/shell/imgcheck.py shell/ 63 | 64 | clean: 65 | rm -rfv .github shell static index.html 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MemeBox

2 | 3 |
Website template for meme collection.
4 | 5 | [简体中文](./README_zh.md) 6 | 7 | * * * 8 | 9 | ### Description 10 | 11 | This template is extracted from [NoneMeme/NoneMeme](https://github.com/NoneMeme/NoneMeme). 12 | 13 | MemeBox can contain various contents including images and markdown documents. 14 | 15 | ### Build 16 | 17 | 1. Prepare a `make` tool. 18 | 19 | 2. Generate your repo with this template. 20 | 21 | 3. Clone your repo. 22 | 23 | 4. Open a shell and switch to the repo directory. 24 | 25 | 5. Run `make`. 26 | 27 | > NOTE: The default `PAGELANG` is now `en` (English). 28 | > 29 | > Read more info in [Custom text](#custom-text). 30 | 31 | 6. Add your contents to `art/` (for documents) or `meme/` (for pictures). 32 | 33 | > NOTE: MemeBox does **not** include any icon files. 34 | > 35 | > You should prepare your icons and put them into `static/` folder. 36 | > 37 | > See the output from make for more information. 38 | 39 | 7. Commit and push. 40 | 41 | ### Custom text 42 | 43 | Usually, you may expect to change some text rather than "MemeBox" etc. 44 | 45 | For this, you can define these temporary environment variables below to override the default: 46 | 47 | |Name|Description| 48 | |:----|:----| 49 | |PAGELANG|Page language mark for .html file, also decides the default text, e.g. "zh"| 50 | |TITLE|The title of your memebox, e.g. "MemeBox"| 51 | |DESC|A short description for your memebox, e.g. "Joy for Everyone"| 52 | |TDESC|A description for the text part| 53 | |FOOTER|Footer on every pages| 54 | 55 | Just like this: 56 | 57 | PAGELANG=en TITLE=Foo DESC=Bar FOOTER="Lorem ipsum" make 58 | 59 | If the default text does not satisfy you, don't worry, they are also customizable with the way above: 60 | 61 | |Name|Description| 62 | |:----|:----| 63 | |T_MEMEPIC|Indicates a hyperlink jumping to picture page| 64 | |T_MEMETXT|Indicates a hyperlink jumping to text page| 65 | |T_DOWNLOAD|A hint for downloading a image| 66 | |T_ANOTHER|A hint for randomly picking a image| 67 | |T_BACK|A hint for leaving the full view| 68 | |T_ZOOMIN|A hint for entering the full view| 69 | |T_NIMGS|Images summary text| 70 | 71 | ### License 72 | 73 | This template is under the [MIT License](LICENSE). 74 | 75 | ### Special thanks 76 | 77 | - **[modcrafts/a60-shop](https://github.com/modcrafts/a60-shop)** 78 | 79 | - **[picocss/pico](https://github.com/picocss/pico/tree/f9e97c0bf430df8fa3f730eb6a6e84f63d4a9b0c)** 80 | 81 | - **[MarketingPipeline/Markdown-Tag](https://github.com/MarketingPipeline/Markdown-Tag)** 82 | 83 | - **[NoneMeme/NoneMeme](https://github.com/NoneMeme/NoneMeme)** 84 | -------------------------------------------------------------------------------- /src/index.html.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | @TITLE@ | @DESC@ 11 | 12 | 13 | 14 | 15 | 16 |
17 |

@TITLE@ | @T_MEMETXT@

18 |
19 | 51 | 56 | 57 |
58 |

@FOOTER@

59 |
60 |
61 | 62 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/shell/imgcheck.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import sys 3 | import os 4 | import json 5 | 6 | # If you want to skip some images, add them here 7 | skip_list = [ 8 | ... 9 | ] 10 | 11 | def d_hash(image, hash_size=8): 12 | image2 = image.convert('L').resize( 13 | (hash_size + 1, hash_size), 14 | Image.LANCZOS, 15 | ) 16 | difference = [] 17 | for row in range(hash_size): 18 | for col in range(hash_size): 19 | pixel_left = image2.getpixel((col, row)) 20 | pixel_right = image2.getpixel((col + 1, row)) 21 | difference.append(pixel_left > pixel_right) 22 | decimal_value = 0 23 | hex_string = [] 24 | for index, value in enumerate(difference): 25 | if value: 26 | decimal_value += 2 ** (index % 8) 27 | if (index % 8) == 7: 28 | hex_string.append(hex(decimal_value)[2:].rjust(2, '0')) 29 | decimal_value = 0 30 | return ''.join(hex_string) 31 | 32 | 33 | def count_similarity(hash1, hash2): 34 | samiliarity = 0 35 | for i in enumerate(hash1): 36 | if i[1] == hash2[i[0]]: 37 | samiliarity += 1 38 | return samiliarity/len(hash1) 39 | 40 | 41 | hash_map = {} 42 | 43 | # Get dHash for each image in meme folder 44 | print('Calculating dHash for each image') 45 | for filename in os.listdir('meme'): 46 | if filename in skip_list: 47 | continue 48 | try: 49 | image = Image.open('meme/' + filename) 50 | hash_map[filename] = d_hash(image, 32) 51 | except: 52 | print(f'Failed to calculate dHash for {filename}') 53 | 54 | # Check for similar images 55 | print('Checking for similar images') 56 | similar_images = [] 57 | for (name, hash1) in hash_map.copy().items(): 58 | new_map = hash_map.copy() 59 | del new_map[name] 60 | for (name2, hash2) in new_map.items(): 61 | samiliarity = count_similarity(hash1, hash2) 62 | if samiliarity > 0.8: 63 | similar_images.append((name, name2, samiliarity)) 64 | 65 | # Collect similar images 66 | output_map = {} 67 | for (name, name2, samiliarity) in similar_images: 68 | if name not in output_map: 69 | output_map[name] = {} 70 | if name2 not in output_map: 71 | output_map[name2] = {} 72 | if not (name2 in output_map[name].keys()): 73 | output_map[name][name2] = samiliarity 74 | if not (name in output_map[name2].keys()): 75 | output_map[name2][name] = samiliarity 76 | 77 | # Sort similar images 78 | output_map = sorted(output_map.items(), key=lambda x: len(x[1]), reverse=True) 79 | 80 | # Print similar images 81 | print('Similar images:') 82 | for (name, similar) in output_map: 83 | print(f'{name} is similar to:') 84 | for (name2, samiliarity) in similar.items(): 85 | print(f' {name2} ({samiliarity*100:.2f}%)') 86 | 87 | # Write dHash map to file 88 | with open('static/data/images/hash_map.json', 'w', encoding='utf8') as f: 89 | json.dump(hash_map, f, indent=4, ensure_ascii=False) 90 | # Write similar images to file 91 | with open('static/data/images/similar_images.json', 'w', encoding='utf8') as f: 92 | json.dump(output_map, f, indent=4, ensure_ascii=False) 93 | 94 | if len(similar_images) == 0: 95 | print('All images are unique') 96 | sys.exit(0) 97 | else: 98 | print('Some images are similar') 99 | sys.exit(0) 100 | -------------------------------------------------------------------------------- /src/static/scripts/index.js.in: -------------------------------------------------------------------------------- 1 | import config from './config.js' 2 | 3 | // const development = 4 | // location.host.search(/.+\.github\.io/) === -1 && 5 | // location.host.search(/nonememe\.icu/) === -1 6 | const memeRegex = /^meme\/((.+)\.(?:jpg|png|jfif|webp|gif|jpeg|bmp))$/i; 7 | // positive lookbehind (?<=) is not supported by all browsers. 8 | // global flag (g) will not capture groups 9 | // [0] is full path (meme/id.jpg) 10 | // [1] is file name (id.jpg) 11 | // [2] is id (id) 12 | const development = false 13 | const domParser = new DOMParser() 14 | /** @type {string[]} */ 15 | let items = [] 16 | let displayedItemCount = 0 17 | 18 | const galleryIO = new IntersectionObserver((entries) => { 19 | if (entries[0].intersectionRatio > 0 && displayedItemCount < items.length) 20 | loadgallery(10) 21 | }) 22 | 23 | /** 24 | * 25 | * @param {number} min 26 | * @param {number} max 27 | * @returns {number} 28 | */ 29 | function random(min, max) { 30 | return Math.floor(Math.random() * (max - min + 1)) + min 31 | } 32 | 33 | /** 34 | * @param {{ 35 | id: string; 36 | src: string; 37 | alt: string; 38 | title: string; 39 | }} obj 40 | * @returns {Element} 41 | */ 42 | function createMemeElement(obj) { 43 | const id = 'gallery-item'; 44 | let temp = document.createElement('div'); 45 | temp.innerHTML = document.getElementById(id).innerHTML; 46 | temp.querySelector("a").href = obj.id; 47 | temp.querySelector("img").src = obj.src; 48 | temp.querySelector("img").alt = obj.alt; 49 | temp.querySelector("span").textContent = obj.title; 50 | return temp.children[0]; 51 | } 52 | 53 | /** 54 | * @param {string} url 55 | * @returns {Promise} 56 | */ 57 | function get(url) { 58 | return new Promise((resolve, reject) => { 59 | const xhr = new XMLHttpRequest() 60 | xhr.open('GET', url) 61 | xhr.addEventListener('load', () => resolve(xhr)) 62 | xhr.addEventListener('error', () => reject(xhr)) 63 | xhr.send() 64 | }) 65 | } 66 | 67 | async function loadgallery(remainItemCount) { 68 | if (remainItemCount <= 0 || displayedItemCount >= items.length) { 69 | if (displayedItemCount >= items.length) { 70 | console.log("已加载全部图片。"); 71 | } 72 | galleryIO.observe(document.getElementById('footer')) 73 | return 74 | } 75 | galleryIO.unobserve(document.getElementById('footer')) 76 | 77 | // 获取高度最小的列 78 | const column = [ 79 | document.getElementById('col1'), 80 | document.getElementById('col2'), 81 | document.getElementById('col3'), 82 | ].sort((a, b) => a.offsetHeight - b.offsetHeight)[0] 83 | 84 | let match = items[displayedItemCount].match(memeRegex); 85 | if (match === null) { 86 | displayedItemCount += 1; 87 | loadgallery(remainItemCount - 1); 88 | return; 89 | } 90 | let filename = match[1]; 91 | let id = match[2]; 92 | const node = createMemeElement({ 93 | id: `#${encodeURIComponent(id)}`, 94 | src: "./meme/" + encodeURIComponent(filename), 95 | alt: items[displayedItemCount], 96 | title: `# ${id}`, 97 | }) 98 | 99 | // 加载好以后再执行下一个图片的加载以保证顺序没问题 100 | node.querySelector('img').addEventListener('load', () => 101 | loadgallery(remainItemCount - 1) 102 | ) 103 | column.append(node) 104 | displayedItemCount += 1 105 | } 106 | 107 | function view() { 108 | const view = document.getElementById('view') 109 | view.style.display = { 110 | true: 'none', 111 | false: 'block', 112 | }[!location.hash || location.hash == '#'] 113 | let name = decodeURIComponent( 114 | location.hash.substring(1, location.hash.length) 115 | ) 116 | view.querySelector('h2').innerText = `# ${name}` 117 | for (const i of items) { 118 | let match = i.match(memeRegex); 119 | if (match[2] === name) { 120 | name = match[1]; 121 | break; 122 | } 123 | } 124 | 125 | let imgSource = "./meme/" + encodeURIComponent(name); 126 | view.querySelector('img').src = imgSource; 127 | view.querySelector('img').alt = name; 128 | view.querySelector('a').href = imgSource; 129 | window.scrollTo({ 130 | top: view.offsetTop, 131 | behavior: 'smooth', 132 | }) 133 | } 134 | 135 | async function initgallery() { 136 | document.getElementById( 137 | 'description' 138 | ).innerHTML = `@DESC@, @T_NIMGS@` 139 | document.getElementById('refresh-btn').onclick = () => { 140 | location.hash = `#${items[random(0, items.length - 1)].match(memeRegex)[2]}` 141 | } 142 | for (let i = 0; i < items.length - 1; i++) { 143 | const j = random(i, items.length - 1) 144 | const temp = items[i] 145 | items[i] = items[j] 146 | items[j] = temp 147 | } 148 | await loadgallery(10) 149 | galleryIO.observe(document.getElementById('footer')) 150 | view() 151 | } 152 | 153 | ; (async () => { 154 | /** 155 | * 判断使用何种 API , 获取图片列表 156 | */ 157 | 158 | // 开发环境(使用 live server) 159 | if (development) { 160 | for (const i of domParser 161 | .parseFromString((await get('../meme/')).response, 'text/html') 162 | .querySelectorAll('#files a.icon-image')) { 163 | items.push( 164 | 'meme/' + decodeURIComponent(i.href.match(memeRegex)[1]) 165 | ) 166 | } 167 | } else { 168 | // 生产环境(使用静态文件) 169 | items = config.items 170 | } 171 | 172 | initgallery() 173 | window.addEventListener('hashchange', view) 174 | })() 175 | --------------------------------------------------------------------------------