<!DOCTYPE html>
<html lang="zh">
<head>
<title>瀑布流图片浏览器</title>
<meta charset="UTF-8" />
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="black" />
<meta name="description" content="Masonry Imgage Viewer" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="favicon.ico" />
<link rel="stylesheet" href="style.css" />
<link rel="manifest" href="app.webmanifest" />
<style>
:root {
--colgap: 0px;
--rowgap: 0px;
--imgradius: 0px;
--imgborder: 0px;
--opacity: 1;
--halfwhite: rgba(255, 255, 255, 0.5);
--halfblack: rgba(0, 0, 0, 0.5);
--bg-color: #000;
--bg-image: none;
}
body {
margin: 0;
background-color: var(--bg-color, #000);
background-image: var(--bg-image, none);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
color: white;
display: flex;
overflow-y: scroll;
}
#hint {
position: fixed;
height: 100%;
width: 100%;
display: grid;
place-items: center;
font-size: xx-large;
}
#cursorplace {
position: fixed;
height: 100%;
width: 1px;
z-index: 1;
}
#indicator {
position: fixed;
top: 0;
right: 17px;
width: 50px;
height: 50px;
place-items: center;
pointer-events: none;
background-color: var(--halfblack);
border-radius: 50%;
font-size: large;
animation: scrollmove linear;
animation-timeline: scroll();
display: none;
}
#indicator.show {
display: grid;
}
@keyframes scrollmove {
from {
top: 0;
}
to {
top: calc(100vh - 50px);
}
}
#imgbox {
flex: 1;
display: flex;
flex-wrap: wrap;
line-height: 0;
column-gap: var(--colgap);
}
.colflex {
align-items: flex-start;
}
.rowflex::after {
content: "";
flex-grow: 114514;
}
.rowflex {
row-gap: var(--rowgap);
}
.imgcol {
flex: 1;
display: flex;
flex-direction: column;
row-gap: var(--rowgap);
}
.wrap {
position: relative;
cursor: pointer;
}
.wrap {
position: relative;
cursor: pointer;
}
.wrap {
position: relative;
cursor: pointer;
}
.wrap > img {
width: 100%;
height: auto;
box-sizing: border-box;
border: solid white;
border-radius: var(--imgradius);
border-width: var(--imgborder);
}
/* 移除悬停显示的样式 */
/* .wrap:hover > .info {
visibility: visible;
} */
.info {
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
line-height: normal;
border-radius: 10px;
background-color: var(--halfblack);
visibility: visible; /* 修改为可见 */
}
.info > span {
cursor: text;
word-break: break-all;
}
#cover {
position: fixed;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
outline: none;
background-color: var(--halfblack);
display: none;
}
#cover.show {
display: block;
}
.zoom {
position: fixed;
}
#previmg,
#nextimg {
top: 50%;
opacity: 0.5;
}
#previmg {
left: 1%;
}
#nextimg {
right: 1%;
}
#previmg:hover,
#nextimg:hover {
opacity: 1;
}
.floatbtn {
position: fixed;
z-index: 1;
width: 50px;
height: 50px;
display: grid;
place-items: center;
outline: none;
border: medium solid white;
border-radius: 50%;
background-color: var(--halfblack);
color: white;
font-size: xx-large;
cursor: pointer;
}
#sidebtn {
top: 20px;
left: 20px;
opacity: var(--opacity);
}
#sidebtn:hover {
opacity: 1;
}
#treebtn {
top: calc(50% - 35px);
left: -30px;
height: 70px;
display: flex;
justify-content: end;
opacity: var(--opacity);
}
#treebtn:hover {
opacity: 1;
}
.scrollbtn {
position: fixed;
z-index: 1;
width: 40px;
height: 40px;
outline: none;
border: medium solid white;
background-color: var(--halfblack);
opacity: var(--opacity);
user-select: none;
cursor: pointer;
}
.scrollbtn:hover {
opacity: 1;
}
#totop {
bottom: 78px;
right: 40px;
border-radius: 10px 10px 0 0;
}
#toend {
bottom: 40px;
right: 40px;
border-radius: 0 0 10px 10px;
}
#sidebar {
position: fixed;
left: -400px;
display: grid;
align-items: center;
grid-auto-rows: min-content;
grid-template-columns: repeat(6, 50px);
column-gap: 10px;
row-gap: 10px;
border-radius: 20px;
padding: 20px;
background-color: var(--halfblack);
white-space: nowrap;
z-index: 1;
}
#sidebar.show {
left: 0;
}
#sidebar > button {
justify-self: center;
outline: none;
padding: 5px 10px 5px 10px;
border: medium solid var(--halfwhite);
border-radius: 50px;
background-color: var(--halfblack);
cursor: pointer;
}
#sidebar > button:hover {
border-color: white;
}
#sidebar > button:active {
background: var(--halfwhite);
}
#sidebar > button.active {
background: var(--halfwhite);
}
#treebar {
position: fixed;
z-index: 2;
background-color: var(--halfblack);
display: none;
}
#treebar.show {
display: block;
}
#resizebar {
min-width: 200px;
max-width: 50vw;
resize: horizontal;
overflow: scroll;
opacity: 0.5;
}
#resizebar::-webkit-scrollbar {
width: 20px;
height: 100vh;
}
#resizebar::-webkit-resizer {
background-color: white;
}
#dirtree {
position: absolute;
width: calc(100% - 25px);
height: 100vh;
direction: rtl;
overflow-y: scroll;
line-height: 1;
padding-right: calc(50vw - 100%);
}
#dirtree > ul {
margin: 0;
}
ul {
direction: ltr;
margin: 0 0 0 10px;
padding: 0;
border-left: medium solid var(--halfwhite);
}
li {
position: relative;
z-index: 1;
min-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
list-style: "📁" inside;
padding-bottom: 2px;
border-radius: 25px;
border: medium solid transparent;
cursor: pointer;
}
li:hover {
width: fit-content;
border-color: white;
background-color: var(--halfblack);
}
li.active {
background-color: var(--halfwhite);
}
li.visited {
opacity: 0.75;
}
select {
justify-self: center;
padding: 5px;
outline: none;
text-align: center;
font-size: medium;
cursor: pointer;
border-radius: 50px;
background-color: var(--halfblack);
border: medium solid var(--halfwhite);
}
select:hover {
border-color: white;
}
[type="text"] {
justify-self: center;
box-sizing: border-box;
width: 100%;
padding: 5px;
outline: none;
text-align: center;
font-size: medium;
border-radius: 50px;
border: medium solid var(--halfwhite);
background-color: var(--halfblack);
}
[type="text"]:hover {
border-color: white;
}
[type="range"] {
appearance: none;
width: 2 00px;
height: 10px;
border-radius: 10px;
border: medium solid white;
background-color: var(--halfblack);
opacity: 0.5;
}
[type="range"]:hover {
opacity: 1;
}
::-webkit-slider-thumb {
appearance: none;
width: 30px;
height: 30px;
cursor: pointer;
border-radius: 50%;
border: medium solid white;
background-color: var(--halfblack);
opacity: 0.5;
}
::-webkit-slider-thumb:hover {
opacity: 1;
}
.span2 {
grid-column: span 2;
}
.span3 {
grid-column: span 3;
}
.span4 {
grid-column: span 4;
}
.tootip {
display: none;
}
.tip:hover .tootip {
display: block;
position: absolute;
background-color: black;
padding: 10px;
border-radius: 10px;
}
::selection {
background-color: white;
color: black;
}
.background-settings {
grid-column: span 6;
display: flex;
flex-wrap: wrap;
gap: 5px;
margin: 5px 0;
align-items: center;
justify-content: start;
}
.background-settings input[type="color"] {
width: 100px;
height: 30px;
padding: 0;
border: none;
border-radius: 4px;
cursor: pointer;
}
.background-settings button {
height: 30px;
padding: 0 10px;
border: 1px solid #666;
border-radius: 4px;
background: #333;
color: #fff;
cursor: pointer;
}
.background-settings button:hover {
background: #444;
}
.tip {
grid-column: span 6;
margin: 5px 0;
}
#sidebar > div:not(.background-settings):not(.tip) {
display: flex;
align-items: center;
}
#sidebar input,
#sidebar button,
#sidebar select {
margin: auto 0;
}
</style>
</head>
<body>
<div id="hint">点击或拖入图片文件夹</div>
<div id="cursorplace"></div>
<div id="imgbox"></div>
<div id="indicator"></div>
<div id="cover" tabindex="1">
<button id="previmg" class="floatbtn"><</button>
<button id="nextimg" class="floatbtn">></button>
</div>
<button class="floatbtn" id="treebtn">></button>
<button class="floatbtn" id="sidebtn">+</button>
<button class="scrollbtn" id="toend"></button>
<button class="scrollbtn" id="totop"></button>
<div id="treebar">
<div id="dirtree">
<ul></ul>
</div>
<div id="resizebar"></div>
</div>
<div id="sidebar">
<div>列距:</div>
<input type="range" id="colgapinput" min="0" max="40" value="20" class="span4" />
<div id="colgap">0</div>
<div>行距:</div>
<input type="range" id="rowgapinput" min="0" max="40" value="20" class="span4" />
<div id="rowgap">0</div>
<div>圆角:</div>
<input type="range" id="imgradiusinput" min="0" max="40" value="20" class="span4" />
<div id="imgradius">0</div>
<div>边框:</div>
<input type="range" id="imgborderinput" min="0" max="5" value="0" class="span4" />
<div id="imgborder">0</div>
<div>列数:</div>
<button id="colflex">纵向</button>
<input type="range" id="colcountinput" min="1" max="8" value="4" class="span3" />
<div id="colcount">4</div>
<div>高度:</div>
<button id="rowflex">横向</button>
<input type="range" id="minheightinput" min="100" max="500" value="300" step="10" class="span3" />
<div id="minheight">250</div>
<div>总数:</div>
<div id="totalcount"></div>
<div>加载:</div>
<div id="loadedcount"></div>
<div>显示:</div>
<div id="showcount"></div>
<button id="loadall">加载</button>
<button id="pause">暂停</button>
<div>跳转:</div>
<input id="jumpTo" type="text" />
<div>每次:</div>
<input id="perload" type="text" value="25" />
<div>排序:</div>
<select id="sortby" class="span2">
<option value="default">默认</option>
<option value="lastModified">日期</option>
<option value="size">大小</option>
<option value="random">随机</option>
<option value="folderRandom">多文件夹随机</option>
</select>
<select id="order" class="span2">
<option value="asc">升序</option>
<option value="desc">降序</option>
</select>
<button id="resort">重排</button>
<div>过滤:</div>
<button id="filtmono" class="span2">黑白漫画</button>
<button id="filtborder" class="span2">纯色边框</button>
<button id="revert">反转</button>
<div class="tip">
背景:
</div>
<div class="background-settings">
<input type="color" id="bgColor" class="span2" value="#000000" />
<!-- <button id="bgImageBtn" class="span2">选择图片</button>
<input type="file" id="bgImageInput" accept="image/*" style="display: none;" /> -->
<button id="clearBg" class="span2">清除背景</button>
</div>
<div class="tip">
宽高:
<div class="tootip">
例:<br />
"16:9"表示宽高比等于16:9<br />
"16:9-"表示宽高比大于16:9<br />
"-16:9"表示宽高比小于16:9<br />
"4:3-16:9"表示宽高比在4:3到16:9之间<br />
</div>
</div>
<input type="text" id="aspectratio" class="span3" />
<div class="tip">自动滚动:</div>
<button id="autoscroll" class="span2">开始</button>
<input type="range" id="scrollspeed" min="1" max="10" value="3" class="span2" />
<div id="speedvalue">3</div>
<select id="scrollMode" class="span2">
<option value="down">向下滚动</option>
<option value="updown">上下往复</option>
</select>
</div>
<script src="script.js"></script>
<script>
//倒排
//缓存
//历史
//复制 移动 删除
navigator.serviceWorker.register("sw.js");
class Queue {
constructor(items = []) {
this.items = items;
this.getters = [];
}
push(item) {
this.items.push(item);
if (this.getters.length > 0) this.getters.shift()(this.items.shift());
}
shift() {
if (this.items.length === 0)
return new Promise((resolve) => this.getters.push(resolve));
return this.items.shift();
}
}
class Semaphore {
constructor(value = 1) {
this.value = value;
this.queue = [];
}
async acquire() {
if (this.value > 0) this.value--;
else return new Promise((resolve) => this.queue.push(resolve));
}
release() {
this.value++;
if (this.queue.length > 0) this.queue.shift()();
}
}
DataView.prototype.getUint24 = function (byteOffset, littleEndian) {
if (littleEndian) {
return (
this.getUint8(byteOffset) |
(this.getUint8(byteOffset + 1) << 8) |
(this.getUint8(byteOffset + 2) << 16)
);
} else {
return (
(this.getUint8(byteOffset) << 16) |
(this.getUint8(byteOffset + 1) << 8) |
this.getUint8(byteOffset + 2)
);
}
};
function throttle(func, ms = 1000) {
let timeout;
let con = this;
return function () {
if (timeout) return;
func.apply(con, arguments);
timeout = setTimeout(() => (timeout = null), ms);
};
}
function debounce(func, ms = 1000) {
let timeout;
let con = this;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(con, arguments), ms);
};
}
function range(start, end, step = 1) {
let arr = [];
for (let i = start; i < end; i += step) arr.push(i);
return arr;
}
function flatObj(obj, path) {
obj = path.split("/").reduce((obj, name) => obj[name], obj);
function recurse(obj) {
for (let key in obj) {
if (typeof obj[key] === "object") recurse(obj[key]);
else arr.push(obj[key]);
}
}
let arr = [];
recurse(obj);
return arr;
}
let MB = 1024 ** 2;
let GB = 1024 ** 3;
function formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
else if (bytes < MB) return `${(bytes / 1024).toFixed(2)} KB`;
else if (bytes < GB) return `${(bytes / MB).toFixed(2)} MB`;
}
let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let log = console.log;
let docEl = document.documentElement;
let getEl = (id) => document.getElementById(id);
let newEl = (tag) => document.createElement(tag);
// #region elIds
let aspectratio = getEl("aspectratio");
let colcountinput = getEl("colcountinput");
let cover = getEl("cover");
let cursorplace = getEl("cursorplace");
let dirtree = getEl("dirtree");
let filtborder = getEl("filtborder");
let filtmono = getEl("filtmono");
let hint = getEl("hint");
let imgbox = getEl("imgbox");
let indicator = getEl("indicator");
let jumpTo = getEl("jumpTo");
let loadall = getEl("loadall");
let loadedcount = getEl("loadedcount");
let minheightinput = getEl("minheightinput");
let nextimg = getEl("nextimg");
let order = getEl("order");
let pause = getEl("pause");
let perload = getEl("perload");
let previmg = getEl("previmg");
let resort = getEl("resort");
let revert = getEl("revert");
let showcount = getEl("showcount");
let sidebar = getEl("sidebar");
let sidebtn = getEl("sidebtn");
let sortby = getEl("sortby");
let toend = getEl("toend");
let totalcount = getEl("totalcount");
let totop = getEl("totop");
let treebar = getEl("treebar");
let treebtn = getEl("treebtn");
let bgColor = getEl("bgColor");
let bgImageBtn = getEl("bgImageBtn");
let bgImageInput = getEl("bgImageInput");
let clearBg = getEl("clearBg");
let autoscroll = getEl("autoscroll");
let scrollspeed = getEl("scrollspeed");
let speedvalue = getEl("speedvalue");
let scrollMode = getEl("scrollMode");
// #endregion
let zoom;
let flextype;
let currdir;
let minCol;
let minR, maxR;
let held = false;
let scrollAnimation = null;
totalcount.value = 0;
loadedcount.value = 0;
showcount.value = 0;
let dircount = 0;
let loadingAll = 0;
let loading = 0;
let filting = 0;
let imgcols = [];
let marks = [];
let allData = new Map();
let toLoad = new Queue();
let visImgs = new Set();
let configs = [
"colgap",
"rowgap",
"imgradius",
"imgborder",
"colcount",
"minheight",
];
let enums = {
colflex: "colflex",
rowflex: "rowflex",
default: "default",
name: "name",
date: "date",
size: "size",
random: "random",
folderRandom: "folderRandom",
asc: "asc",
desc: "desc",
};
let imgObs = new IntersectionObserver((es) => {
es.forEach((e) => {
if (e.isIntersecting) visImgs.add(e.target.index);
else visImgs.delete(e.target.index);
});
});
let dirObs = new IntersectionObserver((es) => {
es.forEach((e) => {
if (e.isIntersecting) {
currdir?.classList.remove("active");
currdir?.classList.add("visited");
currdir = getEl("li" + e.target.index);
currdir.classList.add("active");
}
});
});
function initSort() {
["sortby", "order", "perload"].forEach((id) => {
let store = localStorage.getItem(id);
let select = getEl(id);
if (store) select.value = store;
select.onchange = (e) => localStorage.setItem(id, e.target.value);
});
}
function initFilt() {
["filtmono", "filtborder", "revert"].forEach((id) => {
let store = localStorage.getItem(id);
let button = getEl(id);
button.active = store === "true";
if (button.active) button.classList.add("active");
button.onclick = (e) => {
let button = e.target;
button.classList.toggle("active");
button.active = button.classList.contains("active");
localStorage.setItem(button.id, button.active);
if (!filtmono.active && !filtborder.active && revert.active) return;
reflow();
};
});
}
function initFlex() {
let store = localStorage.getItem("flextype");
flextype = store !== null ? store : enums.colflex;
getEl(flextype).classList.add("active");
imgbox.className = flextype;
["colflex", "rowflex"].forEach((id, i, arr) => {
let el = getEl(id);
el.onclick = (e) => {
let button = e.target;
if (flextype === button.id) return;
flextype = button.id;
localStorage.setItem("flextype", flextype);
imgbox.className = flextype;
arr.forEach((el) => getEl(el).classList.remove("active"));
button.classList.add("active");
reflow();
};
});
}
function initConfig(id) {
let input = getEl(id + "input");
input.oninput = (e) => (getEl(id).innerText = e.target.value);
input.onchange = (e) => {
let val = e.target.value;
configs[id] = val;
docEl.style.setProperty("--" + id, val + "px");
localStorage.setItem(id, val);
};
let store = localStorage.getItem(id);
if (store) input.value = store;
input.onchange({ target: input });
input.oninput({ target: input });
}
function initBackground() {
let storedBgColor = localStorage.getItem("bgColor");
// let storedBgImage = localStorage.getItem("bgImage");
if (storedBgColor) {
bgColor.value = storedBgColor;
docEl.style.setProperty("--bg-color", storedBgColor);
}
// if (storedBgImage) {
// docEl.style.setProperty("--bg-image", `url(${storedBgImage})`);
// }
bgColor.addEventListener("input", (e) => {
let color = e.target.value;
docEl.style.setProperty("--bg-color", color);
localStorage.setItem("bgColor", color);
});
// bgImageBtn.addEventListener("click", () => {
// bgImageInput.click();
// });
// bgImageInput.addEventListener("change", async (e) => {
// let file = e.target.files[0];
// if (!file) return;
// if (!file.type.startsWith("image/")) {
// alert("请选择图片文件");
// return;
// }
// let reader = new FileReader();
// reader.onload = (e) => {
// let imageUrl = e.target.result;
// docEl.style.setProperty("--bg-image", `url(${imageUrl})`);
// localStorage.setItem("bgImage", imageUrl);
// };
// reader.readAsDataURL(file);
// });
clearBg.addEventListener("click", () => {
docEl.style.setProperty("--bg-color", "#000000");
docEl.style.setProperty("--bg-image", "none");
bgColor.value = "#000000";
localStorage.removeItem("bgImage");
localStorage.setItem("bgColor", "#000000");
});
}
document.ondrop = async (e) => {
if (e.dataTransfer.types[0] !== "Files") return;
e.preventDefault();
let items = [...e.dataTransfer.items].map((item) =>
item.getAsFileSystemHandle()
);
initLoad();
await handle(items);
if (sortby.value !== enums.default || order.value !== enums.asc) reflow();
};
document.onpaste = async (e) => {
if (e.clipboardData.types[0] !== "Files") return;
let items = [...e.clipboardData.items].map((item) =>
item.getAsFileSystemHandle()
);
initLoad();
await handle(items);
if (sortby.value !== enums.default || order.value !== enums.asc) reflow();
};
hint.onclick = async () => {
let items = await showDirectoryPicker({
mode: "readwrite",
startIn: "pictures",
});
initLoad();
await handle(items.values());
if (sortby.value !== enums.default || order.value !== enums.asc) reflow();
};
function initLoad() {
if (loadedcount.value > 0) return;
if (sortby.value === enums.default && order.value === enums.asc) reflow();
docEl.style.setProperty("--opacity", "0");
hint.remove();
}
async function handle(items, dir = "", folderUl = dirtree.children[0]) {
for await (let item of items) {
let name = item.name;
let path = dir + "/" + name;
if (allData.has(path)) continue;
if (item.kind === "directory") {
dircount++;
let val = totalcount.value,
index = val + dircount;
let li = newEl("li");
li.innerText = name;
li.id = "li" + index;
li.index = index;
folderUl.appendChild(li);
let ul = newEl("ul");
folderUl.appendChild(ul);
toLoad.push(path);
allData.set(path, index);
await handle(item.values(), path, ul);
if (val === totalcount.value) {
li.style.display = "none";
ul.style.display = "none";
}
}
if (item.kind === "file") {
let file = await item.getFile();
if (!file.type.match(/image.*/)) continue;
totalcount.value++;
file.dir = dir;
file.path = path;
file.index = totalcount.value + dircount;
allData.set(path, { file });
toLoad.push(path);
totalcount.innerText = totalcount.value;
}
}
}
function reflow(index = 0) {
if (filting) return;
filting = 1;
let type = sortby.value;
let ord = order.value;
let imgs = [...imgbox.children];
if (type !== enums.default) {
if (type === enums.folderRandom) {
// 按文件夹分组
let folderGroups = new Map();
toLoad.items.forEach(path => {
let data = allData.get(path);
if (typeof data === "object") {
let dir = data.file.dir;
if (!folderGroups.has(dir)) {
folderGroups.set(dir, []);
}
folderGroups.get(dir).push(path);
}
});
// 随机打乱每个文件夹内的文件
for (let files of folderGroups.values()) {
for (let i = files.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[files[i], files[j]] = [files[j], files[i]];
}
}
// 将所有文件夹的文件交错组合
let maxLength = Math.max(...folderGroups.values().map(arr => arr.length));
let newItems = [];
for (let i = 0; i < maxLength; i++) {
for (let files of folderGroups.values()) {
if (i < files.length) {
newItems.push(files[i]);
}
}
}
toLoad.items = newItems;
} else {
imgs.sort((a, b) => {
if (type === enums.random) {
return Math.random() - 0.5;
}
let va = a.dataset[type];
let vb = b.dataset[type];
return ord === enums.asc ? va - vb : vb - va;
});
}
}
if (!filtmono.active && !filtborder.active && revert.active) return;
imgbox.querySelectorAll(".mark").forEach((el) => {
el.remove();
});
minCol = imgbox;
marks = [];
visImgs.clear();
imgcols = [];
if (flextype === enums.colflex) {
for (let _ of Array(parseInt(colcountinput.value))) {
let imgcol = newEl("div");
imgcol.className = "imgcol";
imgcols.push(imgcol);
imgcol.onmouseout = addInfo;
}
}
imgbox.replaceChildren(...imgcols);
loading = 0;
if (showcount.value > 0) {
toLoad.getters.forEach((rsv) => rsv());
toLoad.items = [...allData.keys()];
showcount.value = 0;
}
if (index > 0) toLoad.items = toLoad.items.slice(index);
if (type !== enums.default && type !== enums.folderRandom)
toLoad.items = toLoad.items
.filter((p) => typeof allData.get(p) === "object")
.sort((a, b) => allData.get(a).file[type] - allData.get(b).file[type]);
if (ord === enums.desc) toLoad.items.reverse();
loadNext();
filting = 0;
}
async function loadNext() {
if (
loading > 0 ||
(!loadingAll &&
docEl.scrollTop + docEl.clientHeight <
minCol.scrollHeight - docEl.clientHeight)
)
return;
for (let _ of Array(parseInt(perload.value))) {
let path = await toLoad.shift();
if (!path) return;
let data = allData.get(path);
if (typeof data === "number") {
let mark = newEl("div");
mark.index = data;
dirObs.observe(mark);
marks.push(mark);
continue;
}
let file = data.file;
let w, h, img;
if (maxR) {
if (file.width) [w, h] = [file.width, file.height];
else {
[w, h, img] = await getWH(file);
[file.width, file.height] = [w, h];
if (img) data.img = img;
}
if (h * minR > w + 2 || h * maxR < w - 2) continue;
}
let wrap = data.wrap;
if (wrap) {
loadImg(wrap);
continue;
}
let onloaded = () => {
URL.revokeObjectURL(img.src);
loadedcount.innerText = loadedcount.value++ + 1;
let wrap = newEl("div");
wrap.className = "wrap";
wrap.appendChild(img);
data.wrap = wrap;
loadImg(wrap);
loading--;
loadNext();
};
if (data.img) {
img = data.img;
onloaded();
} else {
img = new Image();
data.img = img;
loading++;
img.onload = onloaded;
img.onerror = onloaded;
img.src = URL.createObjectURL(file);
}
img.index = file.index;
img.alt = file.name;
img.path = file.path;
}
setTimeout(loadNext, 0);
}
function addInfo(e) {
let img = e.relatedTarget;
if (img?.tagName !== "IMG") return;
let wrap = img.parentElement;
if (wrap.hasInfo) return;
let file = allData.get(img.path).file;
let imgInfo = [
formatSize(file.size),
`${img.naturalWidth}x${img.naturalHeight}`,
file.lastModifiedDate.toLocaleString().replaceAll("/", "-"),
file.name,
file.dir,
];
imgInfo = imgInfo.flatMap((t) => {
let span = newEl("span");
span.innerText = t;
let br = newEl("br");
return [span, br];
});
let infoBar = newEl("div");
infoBar.classList.add("info");
infoBar.replaceChildren(...imgInfo);
wrap.appendChild(infoBar);
wrap.hasInfo = 1;
}
function loadImg(wrap) {
let img = wrap.children[0];
if (
((filtmono.active && isMono(img)) ||
(filtborder.active && isMonoBorder(img))) ^ revert.active
)
return;
imgObs.observe(wrap);
wrap.removeAttribute("style");
showcount.innerText = showcount.value++ + 1;
wrap.id = "img" + showcount.value;
wrap.index = showcount.value;
if (img.index > marks[0]?.index) wrap.appendChild(marks.shift());
if (flextype === enums.colflex) {
minCol = imgcols.reduce((prev, curr) =>
prev.offsetHeight <= curr.offsetHeight ? prev : curr
);
minCol.appendChild(wrap);
} else {
resize(wrap);
imgbox.appendChild(wrap);
}
}
function resize(wrap) {
let img = wrap.children[0],
ratio = img.naturalWidth / img.naturalHeight;
wrap.style.flexBasis = ratio * minheightinput.value + "px";
wrap.style.flexGrow = ratio;
}
function isMonoBorder(img) {
if (img.isMonoBorder !== undefined) return img.isMonoBorder;
let { ctx, width, height } = getThumb(img);
let d = (x, y, w, h) => ctx.getImageData(x, y, w, h).data;
let wasd = [
...d(0, 0, 1, height),
...d(0, 0, width, 1),
...d(width - 1, 0, 1, height),
...d(0, height - 1, width, 1),
];
let bns = [];
for (let i of range(0, wasd.length, 4))
bns.push(Math.round((wasd[i] + wasd[i + 1] + wasd[i + 2]) / 3 / 4));
let counts = bns.reduce(
(acc, curr) => acc.set(curr, (acc.get(curr) || 0) + 1),
new Map()
);
if (Math.max(...counts.values()) > 0.0625 * wasd.length) {
img.isMonoBorder = true;
return true;
}
img.isMonoBorder = false;
return false;
}
function isMono(img) {
if (img.isMono !== undefined) return img.isMono;
let { ctx, width, height } = getThumb(img);
let pixels = width * height,
data = ctx.getImageData(0, 0, width, height).data;
for (let area of range(0, 4)) {
let r = 0,
g = 0,
b = 0;
for (let i of range(area * pixels, (area + 1) * pixels, 4)) {
r += data[i];
g += data[i + 1];
b += data[i + 2];
}
if (Math.max(r, g, b) - Math.min(r, g, b) > pixels) {
img.isMono = false;
return false;
}
}
img.isMono = true;
return true;
}
function getThumb(img, l = 100) {
let data = allData.get(img.path);
if (l === 100) {
let thumb = data.thumb;
if (thumb) return thumb;
}
let canvas = newEl("canvas"),
ctx = canvas.getContext("2d", { willReadFrequently: true }),
wh = [img.naturalWidth, img.naturalHeight],
m = Math.max(...wh),
r = l / m;
wh = wh.map((n) => Math.round(n * r));
[canvas.width, canvas.height] = wh;
ctx.drawImage(img, 0, 0, ...wh);
let thumb = { canvas, ctx, width: wh[0], height: wh[1] };
if (l === 100) data.thumb = thumb;
return thumb;
}
function toggleZoom(e) {
let oriimg = e.target;
if (oriimg.className === "info") {
if (!getSelection().isCollapsed) return;
oriimg = oriimg.parentElement.children[0];
} else if (oriimg.tagName !== "IMG") return;
let rep = new Image();
rep.id = "rep";
rep.width = oriimg.naturalWidth;
rep.height = oriimg.naturalHeight;
oriimg.replaceWith(rep);
zoom = oriimg;
zoom.className = "zoom";
zoom.style.top = (docEl.clientHeight - zoom.height) / 2 + "px";
zoom.style.left = (docEl.clientWidth - zoom.width) / 2 + "px";
zoom.scale =
Math.min(
docEl.clientHeight / zoom.height,
docEl.clientWidth / zoom.width,
1
).toFixed(2) - 0.01;
zoom.style.scale = zoom.scale;
zoom.minscale = zoom.scale;
zoom.style.transform = `translateZ(0)`;
cover.appendChild(zoom);
cover.classList.add("show");
cover.focus();
}
function zoomImg(e) {
e.preventDefault();
if (
(e.deltaY < 0 && zoom.scale > 4) ||
(e.deltaY > 0 && zoom.scale < zoom.minscale)
)
return;
zoom.scale *= e.deltaY < 0 ? 1.25 : 0.8;
moveImg(e);
}
function moveImg(e) {
let t,
l,
ih = zoom.clientHeight * zoom.scale,
iw = zoom.clientWidth * zoom.scale,
dh = docEl.clientHeight,
dw = docEl.clientWidth;
if (ih > dh) {
t = -(ih - dh + 0.2 * dh) * (e.clientY / dh - 0.5);
} else t = 0;
if (iw > dw) {
l = -(iw - dw + 0.2 * dw) * (e.clientX / dw - 0.5);
} else l = 0;
zoom.style.scale = zoom.scale;
zoom.style.translate = `${l}px ${t}px 0px`;
}
function hideCover() {
zoom.removeAttribute("style");
zoom.removeAttribute("class");
getEl("rep").replaceWith(zoom);
cover.classList.remove("show");
}
function copyImg(e) {
let img = e.target;
if (img.tagName !== "IMG") return;
let { canvas } = getThumb(img, 1920);
canvas.toBlob((blob) =>
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })])
);
e.preventDefault();
}
function naviZoom(e) {
e.stopPropagation();
let index = getEl("rep").parentElement.index;
index +=
e.target === nextimg || e.key === "ArrowRight" || e.key === "d" ? 1 : -1;
let wrap = getEl("img" + index);
if (!wrap) return;
if (!visImgs.has(index)) wrap.scrollIntoView();
hideCover();
toggleZoom({ target: wrap.children[0] });
}
async function getWH(file) {
if (file.size < 30) return [0, 0];
let view = new DataView(await file.slice(0, 30).arrayBuffer());
let sign = view.getUint32();
if (sign === 0x89504e47) return [view.getUint32(16), view.getUint32(20)];
else if (sign === 0x47494638)
return [view.getUint16(6, true), view.getUint16(8, true)];
else if (sign >>> 16 === 0x424d)
return [view.getInt32(18, true), view.getInt32(22, true)];
else if (sign === 0x52494646) {
let vp8 = view.getUint32(12);
if (vp8 === 0x56503820)
return [view.getUint16(26, true), view.getUint16(28, true)];
else if (vp8 === 0x56503858)
return [view.getUint24(24, true) + 1, view.getUint24(27, true) + 1];
else if (vp8 === 0x5650384c) {
return [
(view.getUint16(21, true) & 0x3fff) + 1,
((view.getUint24(22, true) >>> 6) & 0x3fff) + 1,
];
}
} else if (sign >>> 8 === 0xffd8ff) {
view = new DataView(await file.slice(0, 128 * 1024).arrayBuffer());
let marker;
let offset = 2;
while (offset < view.byteLength) {
marker = view.getUint16(offset);
offset += 2;
if (marker === 0xffc0 || marker === 0xffc2)
return [view.getUint16(offset + 5), view.getUint16(offset + 3)];
offset += view.getUint16(offset);
}
}
let img = await new Promise((resolve) => {
let img = new Image();
let onloaded = () => {
URL.revokeObjectURL(img.src);
resolve(img);
};
img.onload = onloaded;
img.onerror = onloaded;
img.src = URL.createObjectURL(file);
});
return [img.naturalWidth, img.naturalHeight, img];
}
function parseRatio() {
let arr = [
[" ", ""],
["—", "-"],
["--", "-"],
[":", ":"],
]
.reduce((t, r) => t.replaceAll(...r), aspectratio.value)
.split("-")
.map((t, i) =>
t === "" && i === 1
? Infinity
: t
.split(":")
.map(Number)
.reduce((p, c) => p / c)
)
.sort();
[minR, maxR] = arr.concat(arr);
reflow();
}
initFlex();
initSort();
initFilt();
initBackground();
configs.forEach(initConfig);
imgbox.onmouseout = addInfo;
imgbox.onmouseenter = addInfo;
cursorplace.onmouseout = addInfo;
cover.onwheel = zoomImg;
previmg.onclick = naviZoom;
nextimg.onclick = naviZoom;
cover.onmousemove = moveImg;
cover.onclick = hideCover;
resort.onclick = reflow;
window.onresize = loadNext;
imgbox.onclick = toggleZoom;
document.onscroll = loadNext;
document.ondragend = copyImg;
document.ondrag = (e) => e.preventDefault();
document.ondragover = (e) => e.preventDefault();
document.ondragenter = (e) => e.preventDefault();
pause.onclick = () => (loadingAll = 0);
totop.onclick = () => docEl.scrollTo(0, 0);
toend.onclick = () => docEl.scrollTo(0, docEl.scrollHeight);
sidebtn.onclick = () => sidebar.classList.add("show");
loadall.onclick = () => {
loadingAll = 1;
loadNext();
};
aspectratio.onkeydown = (e) => {
if (e.key === "Enter") parseRatio();
};
treebtn.onclick = () => {
treebar.classList.add("show");
currdir?.scrollIntoView({ block: "center" });
};
jumpTo.onkeydown = (e) => {
if (e.key === "Enter") getEl("img" + parseInt(jumpTo.value)).scrollIntoView();
};
colcountinput.addEventListener("change", () => {
if (flextype === enums.colflex) reflow();
});
minheightinput.addEventListener("change", () => {
if (flextype === enums.rowflex)
requestAnimationFrame(() => {
imgbox.querySelectorAll(".wrap").forEach(resize);
loadNext();
});
});
dirtree.onclick = (e) => {
let li = e.target;
if (li.tagName !== "LI") treebar.classList.remove("show");
else
// else if (sortby.value === enums.default && order.value === enums.asc)
reflow(li.index - 1);
e.stopPropagation();
};
dirtree.onwheel = (e) => {
if (
(dirtree.scrollTop === 0 && e.deltaY < 0) ||
(dirtree.scrollTop + dirtree.clientHeight >= dirtree.scrollHeight &&
e.deltaY > 0)
)
e.preventDefault();
};
document.addEventListener("mousedown", (e) => {
if (e.button === 0) held = true;
});
document.addEventListener("mouseup", (e) => {
if (e.button === 3) scrollBy(0, 0.9 * docEl.clientHeight);
if (e.button === 4) scrollBy(0, -0.9 * docEl.clientHeight);
if (e.button === 0) {
held = false;
indicator.classList.remove("show");
}
e.preventDefault();
});
document.addEventListener("scroll", () => {
if (held) {
let currIndex = Math.min(...visImgs);
indicator.innerText = currIndex;
indicator.classList.add("show");
}
});
document.addEventListener(
"click",
(e) => {
[sidebar, treebar].forEach((el) => {
if (el.classList.contains("show") && !el.contains(e.target)) {
el.classList.remove("show");
e.stopPropagation();
}
});
},
true
);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
if (cover.classList.contains("show")) hideCover();
else if (treebar.classList.contains("show"))
treebar.classList.remove("show");
else if (scrollAnimation) stopAutoScroll();
else sidebar.classList.toggle("show");
}
if (
cover.classList.contains("show") &&
["ArrowLeft", "ArrowRight", "a", "d"].includes(e.key)
)
naviZoom(e);
});
function startAutoScroll() {
if (scrollAnimation) return;
autoscroll.classList.add("active");
docEl.style.cursor = "none";
sidebar.classList.remove("show");
const speed = scrollspeed.value * 0.5;
let direction = 1;
function scroll() {
if (!scrollAnimation) return;
const mode = scrollMode.value;
const isAtBottom = docEl.scrollTop + docEl.clientHeight >= docEl.scrollHeight;
const isAtTop = docEl.scrollTop <= 0;
if (mode === "down") {
if (isAtBottom) {
docEl.scrollTo(0, 0);
} else {
docEl.scrollBy(0, speed);
}
} else if (mode === "updown") {
if (isAtBottom) {
direction = -1;
} else if (isAtTop) {
direction = 1;
}
docEl.scrollBy(0, speed * direction);
}
scrollAnimation = requestAnimationFrame(scroll);
}
scrollAnimation = requestAnimationFrame(scroll);
}
function stopAutoScroll() {
if (scrollAnimation) {
cancelAnimationFrame(scrollAnimation);
scrollAnimation = null;
autoscroll.classList.remove("active");
docEl.style.cursor = "";
}
}
autoscroll.onclick = () => {
if (scrollAnimation) {
stopAutoScroll();
} else {
startAutoScroll();
}
};
scrollspeed.oninput = (e) => {
speedvalue.innerText = e.target.value;
if (scrollAnimation) {
stopAutoScroll();
startAutoScroll();
}
};
document.addEventListener("visibilitychange", () => {
if (document.hidden && scrollAnimation) {
stopAutoScroll();
}
});
scrollMode.onchange = () => {
if (scrollAnimation) {
stopAutoScroll();
startAutoScroll();
}
};
</script>
</body>
</html>
index.html
备用.css
index.html