桌面端下载器重构为工作台布局,并重做在线看阅读器
- 左侧导航轨 + 主视图 + 常驻详情检查器,替换原左右两栏平铺卡片结构 - 拆分为 TaskView / TaskRow / SearchView / SettingsView / DetailPanel / NavRail / AuthScreen,删除 DashBoard、Side、HentaiSearch - 在线看改为全屏阅读器:适应高度/宽度、常驻翻页栏、缩略图与标题信息, 翻页改为状态驱动,不再点击内部 DOM;补键盘快捷键与大图查看 - 提交弹层默认选中原图(无原图时选体积最大的一个) - 管理员可见「下载人」筛选与详情里的下载人昵称,判定改用后端 isAdmin - store.deleteGallery 返回 promise,便于删除后清理选中态 - 新增 thumbnail 工具与 4 项单测
This commit is contained in:
+58
-17
@@ -1,11 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, onBeforeUnmount} from "vue";
|
import {computed, onMounted, onBeforeUnmount} from "vue";
|
||||||
import store from "./store/index.js";
|
import store from "./store/index.js";
|
||||||
|
import AuthScreen from "./components/AuthScreen.vue";
|
||||||
|
import NavRail from "./components/NavRail.vue";
|
||||||
|
import TaskView from "./components/TaskView.vue";
|
||||||
|
import SearchView from "./components/SearchView.vue";
|
||||||
|
import SettingsView from "./components/SettingsView.vue";
|
||||||
|
import DetailPanel from "./components/DetailPanel.vue";
|
||||||
|
import OnlineReader from "./components/OnlineReader.vue";
|
||||||
|
|
||||||
const resumeProgress = () => {
|
const resumeProgress = () => {
|
||||||
if (store.state.isAuth && document.visibilityState === 'visible') store.dispatch("initWebsocket");
|
if (store.state.isAuth && document.visibilityState === 'visible') store.dispatch("initWebsocket");
|
||||||
};
|
};
|
||||||
const pauseProgress = () => store.dispatch("disconnectWebsocket");
|
const pauseProgress = () => store.dispatch("disconnectWebsocket");
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
restoreSettings();
|
||||||
window.addEventListener('online', resumeProgress);
|
window.addEventListener('online', resumeProgress);
|
||||||
window.addEventListener('pageshow', resumeProgress);
|
window.addEventListener('pageshow', resumeProgress);
|
||||||
window.addEventListener('pagehide', pauseProgress);
|
window.addEventListener('pagehide', pauseProgress);
|
||||||
@@ -18,23 +27,55 @@ onBeforeUnmount(() => {
|
|||||||
document.removeEventListener('visibilitychange', resumeProgress);
|
document.removeEventListener('visibilitychange', resumeProgress);
|
||||||
pauseProgress();
|
pauseProgress();
|
||||||
});
|
});
|
||||||
import Side from "./components/Side.vue";
|
|
||||||
import DashBoard from "./components/DashBoard.vue";
|
// Preferences are persisted in SettingsView; this restores them on load.
|
||||||
|
function restoreSettings() {
|
||||||
|
const auth = localStorage.getItem("auth");
|
||||||
|
store.state.lengthPerPage = localStorage.getItem("lengthPerPage") === null
|
||||||
|
? 30
|
||||||
|
: Number(localStorage.getItem("lengthPerPage"));
|
||||||
|
store.state.category = localStorage.getItem("category") || "myDownload";
|
||||||
|
store.state.sortType = localStorage.getItem("sortType") || "createTime";
|
||||||
|
store.state.galleryNameType = localStorage.getItem("galleryNameType") || "shortName";
|
||||||
|
|
||||||
|
const darkConfigStr = localStorage.getItem("darkConfig");
|
||||||
|
let followSystem = false;
|
||||||
|
if (darkConfigStr !== null) {
|
||||||
|
try {
|
||||||
|
followSystem = Boolean(JSON.parse(darkConfigStr).followSystem);
|
||||||
|
} catch {
|
||||||
|
followSystem = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (followSystem && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
else
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
|
||||||
|
if (auth !== null) store.dispatch("validate", auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadComplete = computed(() => store.state.loadComplete);
|
||||||
|
const activeView = computed(() => store.state.activeView);
|
||||||
|
const selectedGid = computed(() => store.state.selectedGallery?.gid ?? null);
|
||||||
|
|
||||||
|
function openTask(gallery) {
|
||||||
|
store.commit("_selectGallery", gallery);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container class="app-layout">
|
<AuthScreen :load-complete="loadComplete" />
|
||||||
<!-- The task table needs room for name + status + progress + actions; the
|
|
||||||
column grows with the viewport but never squeezes the table. -->
|
|
||||||
<!-- Fixed columns need ~380px; the name column keeps at least 200px. -->
|
|
||||||
<el-aside width="clamp(700px, 52vw, 1000px)" class="side-area">
|
|
||||||
<Side/>
|
|
||||||
</el-aside>
|
|
||||||
<el-main class="main-area">
|
|
||||||
<DashBoard/>
|
|
||||||
</el-main>
|
|
||||||
</el-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
<div class="app" v-show="loadComplete">
|
||||||
</style>
|
<NavRail />
|
||||||
|
<main class="workspace">
|
||||||
|
<TaskView v-show="activeView === 'tasks'" :selected-gid="selectedGid" @open="openTask" />
|
||||||
|
<SearchView v-show="activeView === 'search'" />
|
||||||
|
<SettingsView v-show="activeView === 'settings'" />
|
||||||
|
</main>
|
||||||
|
<DetailPanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OnlineReader />
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ const PATHS = {
|
|||||||
"chevrons-right": ["M13 17l5-5-5-5", "M6 17l5-5-5-5"],
|
"chevrons-right": ["M13 17l5-5-5-5", "M6 17l5-5-5-5"],
|
||||||
sun: ["M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z", "M12 2v2", "M12 20v2", "M4.5 4.5l1.4 1.4", "M18.1 18.1l1.4 1.4", "M2 12h2", "M20 12h2", "M4.5 19.5l1.4-1.4", "M18.1 5.9l1.4-1.4"],
|
sun: ["M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z", "M12 2v2", "M12 20v2", "M4.5 4.5l1.4 1.4", "M18.1 18.1l1.4 1.4", "M2 12h2", "M20 12h2", "M4.5 19.5l1.4-1.4", "M18.1 5.9l1.4-1.4"],
|
||||||
moon: ["M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"],
|
moon: ["M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"],
|
||||||
|
list: ["M8 6h13", "M8 12h13", "M8 18h13", "M3 6h.01", "M3 12h.01", "M3 18h.01"],
|
||||||
|
settings: ["M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"],
|
||||||
|
sort: ["M11 5h10", "M11 9h7", "M11 13h4", "M3 17l3 3 3-3", "M6 18V4"],
|
||||||
|
clock: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 7v5l3 2"],
|
||||||
|
close: ["M18 6L6 18", "M6 6l12 12"],
|
||||||
|
image: ["M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", "M8.5 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z", "M21 15l-5-5L5 21"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
|
||||||
|
const AuthCode = ref("");
|
||||||
|
const isRemember = ref(false);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
loadComplete: {type: Boolean, default: false},
|
||||||
|
});
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
if (AuthCode.value.trim() === "") {
|
||||||
|
ElMessage("请输入授权码后再验证");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch("validate", AuthCode.value);
|
||||||
|
if (isRemember.value) localStorage.setItem("auth", AuthCode.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-show="!loadComplete" class="auth-container">
|
||||||
|
<div class="auth-card">
|
||||||
|
<div class="auth-brand">
|
||||||
|
<span class="auth-mark" aria-hidden="true">L</span>
|
||||||
|
<span class="auth-name">LionWebsite</span>
|
||||||
|
</div>
|
||||||
|
<h1>下载管理器</h1>
|
||||||
|
<p class="auth-subtitle">输入授权码以继续</p>
|
||||||
|
<div class="auth-form">
|
||||||
|
<el-input v-model="AuthCode"
|
||||||
|
placeholder="请输入授权码"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
@keydown.enter="validate" />
|
||||||
|
<el-checkbox v-model="isRemember">记住授权码</el-checkbox>
|
||||||
|
<el-button @click="validate" type="primary" size="large" class="auth-btn">验证</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,423 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- Auth Screen -->
|
|
||||||
<div v-show="!loadComplete" class="auth-container">
|
|
||||||
<div class="auth-card">
|
|
||||||
<div class="auth-brand">
|
|
||||||
<span class="auth-mark" aria-hidden="true">L</span>
|
|
||||||
<span class="auth-name">LionWebsite</span>
|
|
||||||
</div>
|
|
||||||
<h1>下载管理器</h1>
|
|
||||||
<p class="auth-subtitle">输入授权码以继续</p>
|
|
||||||
<div class="auth-form">
|
|
||||||
<el-input v-model="AuthCode"
|
|
||||||
placeholder="请输入授权码"
|
|
||||||
size="large"
|
|
||||||
show-password
|
|
||||||
@keydown.enter="validate" />
|
|
||||||
<el-checkbox v-model="isRemember">记住授权码</el-checkbox>
|
|
||||||
<el-button @click="validate" type="primary" size="large" class="auth-btn">验证</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dashboard Main -->
|
|
||||||
<div v-show="loadComplete" class="dashboard">
|
|
||||||
<div class="control-card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h3 class="section-title">本周用量</h3>
|
|
||||||
<el-button @click="queryWeekUsedAmount" size="small" text>刷新</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="usage-line">
|
|
||||||
<span class="usage-value">{{ weekUsed.weekUsedAmount ?? '—' }}</span>
|
|
||||||
<span class="usage-note">上次重置 {{ weekUsed.lastResetAmountTime ?? '—' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
|
||||||
<div class="control-card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h3 class="section-title">查询任务</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-row">
|
|
||||||
<el-select v-model="type" @change="resetLocalQuery" size="small" style="width: 92px">
|
|
||||||
<el-option value="link" label="链接"/>
|
|
||||||
<el-option value="keyword" label="关键字"/>
|
|
||||||
</el-select>
|
|
||||||
<el-input v-model="param"
|
|
||||||
size="small"
|
|
||||||
placeholder="输入链接或关键字"
|
|
||||||
style="flex: 1; min-width: 150px"
|
|
||||||
@keydown.enter="type === 'link' ? queryRemoteTask() : queryLocalTask()" />
|
|
||||||
<el-button @click="queryRemoteTask" v-show="type === 'link'" size="small" type="primary">远程查询</el-button>
|
|
||||||
<el-button @click="queryLocalTask" size="small">当前页查询</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h3 class="section-title">管理</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-row">
|
|
||||||
<el-button @click="isAlterAuthCode = true" size="small">修改授权码</el-button>
|
|
||||||
<el-button @click="deleteAuthCode" size="small">删除本地授权码</el-button>
|
|
||||||
<el-button @click="isConfig = true" size="small">配置</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h3 class="section-title">系统</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-row">
|
|
||||||
<el-button @click="isQuerying = true" size="small" type="primary">在线搜索</el-button>
|
|
||||||
<el-button @click="reconnect" size="small">重连节点</el-button>
|
|
||||||
<el-button v-if="isLion" @click="resetUndone" size="small" type="danger" plain>重置任务</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h3 class="section-title">外观</h3>
|
|
||||||
</div>
|
|
||||||
<div class="setting-row">
|
|
||||||
<span>夜间模式</span>
|
|
||||||
<el-switch v-model="isDark" @change="toggleStyle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="thumbnailGallery.thumb_link !== undefined" class="control-card">
|
|
||||||
<div class="card-head">
|
|
||||||
<h3 class="section-title">缩略图</h3>
|
|
||||||
</div>
|
|
||||||
<div class="thumbnail-body">
|
|
||||||
<p class="gallery-name">{{ thumbnailGallery.shortName }}</p>
|
|
||||||
<div class="thumbnail-frame">
|
|
||||||
<el-image :src="thumbnailGallery.thumb_link"
|
|
||||||
:preview-src-list="[thumbnailGallery.thumb_link]"
|
|
||||||
style="width: 100%; height: 420px"
|
|
||||||
fit="contain" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-dialog title="任务详情" v-model="chosenGallery" width="560px">
|
|
||||||
<div class="detail-layout">
|
|
||||||
<el-image v-show="chosenGallery.thumb_link !== undefined"
|
|
||||||
class="detail-thumb"
|
|
||||||
fit="contain"
|
|
||||||
:src="chosenGallery.thumb_link !== undefined ? thumbnailUrl(chosenGallery.thumb_link) : ''" />
|
|
||||||
<dl class="detail-list">
|
|
||||||
<dt>名字</dt>
|
|
||||||
<dd>{{ chosenGallery.name }}</dd>
|
|
||||||
<dt>页数</dt>
|
|
||||||
<dd>{{ chosenGallery.pages }}</dd>
|
|
||||||
<dt>语言</dt>
|
|
||||||
<dd>{{ chosenGallery.language }}</dd>
|
|
||||||
<dt>大小</dt>
|
|
||||||
<dd>{{ chosenGallery.fileSize }}</dd>
|
|
||||||
<dt>状态</dt>
|
|
||||||
<dd>{{ chosenGallery.status }}</dd>
|
|
||||||
<template v-if="chosenGallery.availableResolution">
|
|
||||||
<dt>目标分辨率</dt>
|
|
||||||
<dd>
|
|
||||||
<el-select v-model="targetResolution" style="width: 220px">
|
|
||||||
<el-option v-for="(fileSize, resolution) in chosenGallery.availableResolution"
|
|
||||||
:key="resolution"
|
|
||||||
:value="resolution"
|
|
||||||
:label="resolution + ' ' + fileSize" />
|
|
||||||
</el-select>
|
|
||||||
</dd>
|
|
||||||
</template>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="postTask" v-if="chosenGallery.availableResolution" type="primary">提交下载</el-button>
|
|
||||||
<el-button @click="downloadChosenGallery" v-else-if="chosenGallery.status === '下载完成'" type="primary">下载文件</el-button>
|
|
||||||
<el-button @click="readOnlineGallery(chosenGallery)">在线预览</el-button>
|
|
||||||
<el-button v-if="chosenGallery.status === '下载完成'" type="danger" plain @click="deleteGallery">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog title="修改授权码" v-model="isAlterAuthCode">
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="当前授权码">
|
|
||||||
<span class="mono-value">{{ realAuthCode }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新的授权码">
|
|
||||||
<el-input v-model="newAuthCode" show-password></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="再次输入授权码">
|
|
||||||
<el-input v-model="tempAuthCode" show-password></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="isAlterAuthCode = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="alterAuthCode">提交</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<HentaiSearch v-model:is-querying="isQuerying" @close="isQuerying = false"></HentaiSearch>
|
|
||||||
|
|
||||||
<el-dialog title="配置" v-model="isConfig">
|
|
||||||
<el-form label-position="top" class="config-form">
|
|
||||||
<h4 class="section-title">外观</h4>
|
|
||||||
<div class="setting-row">
|
|
||||||
<span>夜间模式跟随系统</span>
|
|
||||||
<el-switch v-model="darkConfig.followSystem"></el-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="section-title">在线预览</h4>
|
|
||||||
<el-form-item label="每页图片数量">
|
|
||||||
<el-input-number v-model="lengthPerPage" :min="1" :max="30" :step="1" style="width: 160px" />
|
|
||||||
<span class="field-hint">1 – 30</span>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h4 class="section-title">默认设置</h4>
|
|
||||||
<el-form-item label="分类">
|
|
||||||
<el-select v-model="category" default-first-option style="width: 220px">
|
|
||||||
<el-option label="全部" value="total"/>
|
|
||||||
<el-option label="我的下载" value="myDownload"/>
|
|
||||||
<el-option label="我的收藏" value="myCollect"/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="排序方式">
|
|
||||||
<el-select v-model="sortType" default-first-option style="width: 220px">
|
|
||||||
<el-option label="名字" value="name"/>
|
|
||||||
<el-option label="简洁名字" value="shortName"/>
|
|
||||||
<el-option label="任务创建时间" value="createTime"/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="显示类型">
|
|
||||||
<el-select v-model="galleryNameType" default-first-option style="width: 220px">
|
|
||||||
<el-option label="名字" value="name"/>
|
|
||||||
<el-option label="简洁名字" value="shortName"/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button type="primary" @click="saveConfig">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import store from "../store";
|
|
||||||
import {computed, ref, onMounted, watch} from "vue";
|
|
||||||
import {ElMessage} from "element-plus"
|
|
||||||
import HentaiSearch from "./HentaiSearch.vue";
|
|
||||||
import {validateLink} from "../utils/validate.js";
|
|
||||||
|
|
||||||
let AuthCode = ref("")
|
|
||||||
let isRemember = ref(false)
|
|
||||||
let isAlterAuthCode = ref(false)
|
|
||||||
let newAuthCode = ref("")
|
|
||||||
let tempAuthCode = ref("")
|
|
||||||
|
|
||||||
let isQuerying = ref(false)
|
|
||||||
let isConfig = ref(false)
|
|
||||||
let isDark = ref(false)
|
|
||||||
let keyword = ref("")
|
|
||||||
let darkConfig = ref({})
|
|
||||||
let lengthPerPage = ref(0)
|
|
||||||
let category = ref("")
|
|
||||||
let sortType = ref("")
|
|
||||||
let galleryNameType = ref("")
|
|
||||||
|
|
||||||
let type = ref("link")
|
|
||||||
let param = ref("")
|
|
||||||
|
|
||||||
let targetResolution = ref("")
|
|
||||||
|
|
||||||
let realAuthCode = computed(() => {
|
|
||||||
return store.state.AuthCode
|
|
||||||
})
|
|
||||||
|
|
||||||
let chosenGallery = computed(() => {
|
|
||||||
return store.state.chosenGallery
|
|
||||||
})
|
|
||||||
|
|
||||||
let loadComplete = computed(() => {
|
|
||||||
return store.state.loadComplete
|
|
||||||
})
|
|
||||||
let weekUsed = computed(() => {
|
|
||||||
return store.state.weekUsed
|
|
||||||
})
|
|
||||||
|
|
||||||
let thumbnailGallery = computed(() => {
|
|
||||||
return store.state.thumbnailGallery
|
|
||||||
})
|
|
||||||
|
|
||||||
let isLion = computed(() => {
|
|
||||||
return store.state.userId === 3
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(chosenGallery, () => {
|
|
||||||
param.value = ''
|
|
||||||
targetResolution.value = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
function reconnect(){
|
|
||||||
store.dispatch("reconnect")
|
|
||||||
}
|
|
||||||
|
|
||||||
function alterAuthCode(){
|
|
||||||
if(newAuthCode.value.trim() === "" || tempAuthCode.value.trim() === "" || newAuthCode.value !== tempAuthCode.value)
|
|
||||||
ElMessage("请检查授权码输入是否错误")
|
|
||||||
else {
|
|
||||||
store.dispatch("alterAuthCode", newAuthCode.value)
|
|
||||||
isAlterAuthCode.value = false
|
|
||||||
newAuthCode.value = ""
|
|
||||||
tempAuthCode.value = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryWeekUsedAmount(){
|
|
||||||
store.dispatch("loadWeekUsedAmount")
|
|
||||||
}
|
|
||||||
|
|
||||||
function postTask(){
|
|
||||||
if(!validateLink(chosenGallery.value.link)){
|
|
||||||
ElMessage("链接错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(targetResolution.value === ''){
|
|
||||||
ElMessage("请选择分辨率再提交")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.dispatch("postGalleryTask",
|
|
||||||
{link: chosenGallery.value.link,
|
|
||||||
targetResolution: targetResolution.value})
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryRemoteTask(){
|
|
||||||
if(!validateLink(param.value)){
|
|
||||||
ElMessage("链接错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(param.value.includes("e-hentai"))
|
|
||||||
param.value = param.value.replace("e-hentai", "exhentai")
|
|
||||||
store.dispatch("queryGalleryTask", param.value)
|
|
||||||
}
|
|
||||||
function queryLocalTask(){
|
|
||||||
switch (type.value){
|
|
||||||
case "link":
|
|
||||||
store.commit("_searchLocalByLink", param.value)
|
|
||||||
break
|
|
||||||
case "keyword":
|
|
||||||
store.commit("_searchLocalByKeyword", param.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetLocalQuery(){
|
|
||||||
store.commit("_searchLocalByKeyword", "")
|
|
||||||
param.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteGallery(){
|
|
||||||
store.dispatch("deleteGallery", chosenGallery.value.gid)
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadChosenGallery(){
|
|
||||||
if(chosenGallery.value.download)
|
|
||||||
window.open(chosenGallery.value.download)
|
|
||||||
else
|
|
||||||
ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"})
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate(){
|
|
||||||
if(AuthCode.value.trim() === ""){
|
|
||||||
ElMessage("请输入授权码后再验证")
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
store.dispatch("validate", AuthCode.value)
|
|
||||||
if(isRemember.value)
|
|
||||||
localStorage.setItem("auth", AuthCode.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readOnlineGallery(gallery){
|
|
||||||
store.dispatch("readOnlineGallery", gallery)
|
|
||||||
}
|
|
||||||
|
|
||||||
function thumbnailUrl(path) {
|
|
||||||
return "https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?" + new URLSearchParams({
|
|
||||||
path,
|
|
||||||
AuthCode: store.state.AuthCode
|
|
||||||
}).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetUndone(){
|
|
||||||
store.dispatch("resetUndone").then()
|
|
||||||
}
|
|
||||||
function deleteAuthCode(){
|
|
||||||
localStorage.removeItem('auth')
|
|
||||||
ElMessage("删除授权码完成")
|
|
||||||
}
|
|
||||||
function toggleStyle(){
|
|
||||||
if(isDark.value)
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
else
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const auth = localStorage.getItem("auth")
|
|
||||||
adjustForStyle()
|
|
||||||
store.state.lengthPerPage = localStorage.getItem("lengthPerPage")
|
|
||||||
store.state.lengthPerPage = store.state.lengthPerPage === null ? 30: Number(store.state.lengthPerPage)
|
|
||||||
category.value = store.state.category = localStorage.getItem("category") === null ? "myDownload" : localStorage.getItem("category")
|
|
||||||
sortType.value = store.state.sortType = localStorage.getItem("sortType") === null ? "createTime" : localStorage.getItem("sortType")
|
|
||||||
galleryNameType.value = store.state.galleryNameType = localStorage.getItem("galleryNameType") === null ? "shortName" : localStorage.getItem("galleryNameType")
|
|
||||||
lengthPerPage.value = store.state.lengthPerPage
|
|
||||||
|
|
||||||
if(auth !== null){
|
|
||||||
store.dispatch("validate", auth)
|
|
||||||
AuthCode.value = localStorage.getItem("auth")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function adjustForStyle(){
|
|
||||||
let darkConfigStr = localStorage.getItem("darkConfig")
|
|
||||||
if(darkConfigStr !== null) {
|
|
||||||
darkConfig.value = JSON.parse(darkConfigStr)
|
|
||||||
if (darkConfig.value.followSystem && isSystemDark())
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
else
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
}else {
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
darkConfig.value = {'followSystem': false}
|
|
||||||
}
|
|
||||||
isDark.value = document.documentElement.classList.contains('dark')
|
|
||||||
}
|
|
||||||
function isSystemDark(){
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
}
|
|
||||||
function saveConfig(){
|
|
||||||
if(lengthPerPage.value < 0 || lengthPerPage.value > 30) {
|
|
||||||
ElMessage("分页页数设置错误,范围1~30")
|
|
||||||
lengthPerPage.value = 30
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
store.state.lengthPerPage = Number(lengthPerPage.value)
|
|
||||||
localStorage.setItem("lengthPerPage", lengthPerPage.value)
|
|
||||||
}
|
|
||||||
localStorage.setItem("darkConfig", JSON.stringify(darkConfig.value))
|
|
||||||
localStorage.setItem("category", category.value)
|
|
||||||
localStorage.setItem("sortType", sortType.value)
|
|
||||||
localStorage.setItem("galleryNameType", galleryNameType.value)
|
|
||||||
|
|
||||||
store.commit("_setCategory", category.value)
|
|
||||||
store.commit("_setSortType", sortType.value)
|
|
||||||
store.commit("_setGalleryNameType", galleryNameType.value)
|
|
||||||
|
|
||||||
isConfig.value = false
|
|
||||||
adjustForStyle()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import AppIcon from "./AppIcon.vue";
|
||||||
|
import {ehThumbnailUrl, pickDefaultResolution} from "../utils/thumbnail.js";
|
||||||
|
|
||||||
|
const targetResolution = ref("");
|
||||||
|
const isRetrying = ref(false);
|
||||||
|
|
||||||
|
// Two flows share this panel: a task already in the list, and a remote gallery
|
||||||
|
// that was resolved from a link or from the online search.
|
||||||
|
const task = computed(() => store.state.selectedGallery);
|
||||||
|
const remote = computed(() => store.state.chosenGallery || null);
|
||||||
|
|
||||||
|
// A freshly resolved gallery is a pending decision, so it takes the panel over
|
||||||
|
// even if a task was selected earlier.
|
||||||
|
const showRemote = computed(() => Boolean(remote.value));
|
||||||
|
|
||||||
|
watch(remote, gallery => {
|
||||||
|
targetResolution.value = gallery
|
||||||
|
? pickDefaultResolution(gallery.availableResolution)
|
||||||
|
: "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin-only fields come from the backend's isAdmin flag, not a hardcoded id.
|
||||||
|
const isAdmin = computed(() => store.state.isAdmin);
|
||||||
|
|
||||||
|
const isDone = computed(() => Boolean(task.value && task.value.status === "下载完成"));
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = task.value?.status;
|
||||||
|
if (status === "下载完成") return "is-done";
|
||||||
|
if (status === "下载中" || status === "压缩中") return "is-running";
|
||||||
|
return "is-waiting";
|
||||||
|
});
|
||||||
|
|
||||||
|
const percent = computed(() => {
|
||||||
|
const item = task.value;
|
||||||
|
if (!item || item.status !== "下载中" || !item.pages) return null;
|
||||||
|
const done = Number(item.proceeding) || 0;
|
||||||
|
return Math.min(100, Math.max(0, Math.round((done / item.pages) * 100)));
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolutions = computed(() => {
|
||||||
|
const gallery = remote.value;
|
||||||
|
if (!gallery || !gallery.availableResolution) return [];
|
||||||
|
return Object.entries(gallery.availableResolution).map(([resolution, fileSize]) => ({
|
||||||
|
resolution,
|
||||||
|
fileSize,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote galleries carry a raw path; the proxy URL needs the active auth code.
|
||||||
|
const remoteThumb = computed(() => {
|
||||||
|
const gallery = remote.value;
|
||||||
|
if (!gallery) return "";
|
||||||
|
return ehThumbnailUrl(gallery.thumb_link || gallery.thumbnailUrl, store.state.AuthCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeInspector() {
|
||||||
|
if (remote.value) store.commit("_setChosenGallery", {gallery: false});
|
||||||
|
else store.commit("_selectGallery", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTask() {
|
||||||
|
if (!task.value) return;
|
||||||
|
if (task.value.download) window.open(task.value.download);
|
||||||
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadRemote() {
|
||||||
|
const gallery = remote.value;
|
||||||
|
if (!gallery) return;
|
||||||
|
if (gallery.download) window.open(gallery.download);
|
||||||
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOnline(gallery) {
|
||||||
|
if (!gallery) return;
|
||||||
|
store.dispatch("readOnlineGallery", gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCollect() {
|
||||||
|
if (!task.value) return;
|
||||||
|
const gallery = task.value;
|
||||||
|
if (gallery.isCollect) store.dispatch("disCollectGallery", gallery.gid);
|
||||||
|
else store.dispatch("collectGallery", gallery.gid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTask() {
|
||||||
|
if (!task.value) return;
|
||||||
|
store.dispatch("deleteGallery", task.value.gid).then(() => store.commit("_selectGallery", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryTask() {
|
||||||
|
if (!task.value || isRetrying.value) return;
|
||||||
|
isRetrying.value = true;
|
||||||
|
try {
|
||||||
|
await store.dispatch("retryGallery", task.value.gid);
|
||||||
|
} finally {
|
||||||
|
isRetrying.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitTask() {
|
||||||
|
const gallery = remote.value;
|
||||||
|
if (!gallery) return;
|
||||||
|
if (targetResolution.value === "") {
|
||||||
|
ElMessage("请选择分辨率再提交");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch("postGalleryTask", {
|
||||||
|
link: gallery.link,
|
||||||
|
targetResolution: targetResolution.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<aside class="inspector" aria-label="详情">
|
||||||
|
<template v-if="showRemote">
|
||||||
|
<header class="inspector-bar">
|
||||||
|
<h2 class="section-title">远端任务</h2>
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
<button type="button" class="icon-button" title="取消" aria-label="取消" @click="closeInspector">
|
||||||
|
<AppIcon name="close" :size="15" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="inspector-scroll">
|
||||||
|
<div class="preview-frame">
|
||||||
|
<img v-if="remoteThumb" :src="remoteThumb" alt="" loading="lazy">
|
||||||
|
<span v-else class="preview-empty">
|
||||||
|
<AppIcon name="image" :size="22" />无缩略图
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="inspector-title">{{ remote.name }}</h3>
|
||||||
|
|
||||||
|
<div class="inspector-badges">
|
||||||
|
<span v-if="remote.status" class="status-pill is-waiting">{{ remote.status }}</span>
|
||||||
|
<span v-if="remote.pages" class="inspector-badge">{{ remote.pages }} 页</span>
|
||||||
|
<span v-if="remote.fileSize" class="inspector-badge">{{ remote.fileSize }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="resolutions.length">
|
||||||
|
<p class="section-title inspector-label">目标分辨率</p>
|
||||||
|
<div class="resolution-list">
|
||||||
|
<button v-for="item in resolutions"
|
||||||
|
:key="item.resolution"
|
||||||
|
type="button"
|
||||||
|
:aria-pressed="targetResolution === item.resolution"
|
||||||
|
@click="targetResolution = item.resolution">
|
||||||
|
<span>{{ item.resolution }}</span>
|
||||||
|
<span class="resolution-size">{{ item.fileSize }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inspector-actions is-remote">
|
||||||
|
<template v-if="resolutions.length">
|
||||||
|
<el-button type="primary" @click="submitTask">提交下载</el-button>
|
||||||
|
<el-button @click="readOnline(remote)">
|
||||||
|
<AppIcon name="eye" :size="15" />在线看
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-button type="primary" @click="downloadRemote">
|
||||||
|
<AppIcon name="download" :size="15" />下载文件
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="readOnline(remote)">
|
||||||
|
<AppIcon name="eye" :size="15" />在线看
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="task">
|
||||||
|
<header class="inspector-bar">
|
||||||
|
<h2 class="section-title">任务详情</h2>
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
<button type="button" class="icon-button" title="收起详情" aria-label="收起详情" @click="closeInspector">
|
||||||
|
<AppIcon name="close" :size="15" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="inspector-scroll">
|
||||||
|
<div class="preview-frame">
|
||||||
|
<img v-if="task.thumb_link" :src="task.thumb_link" alt="" loading="lazy">
|
||||||
|
<span v-else class="preview-empty">
|
||||||
|
<AppIcon name="image" :size="22" />无缩略图
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="inspector-title">{{ task.name }}</h3>
|
||||||
|
|
||||||
|
<div class="inspector-badges">
|
||||||
|
<span class="status-pill" :class="statusClass">{{ task.status }}</span>
|
||||||
|
<span class="inspector-badge">{{ task.pages }} 页</span>
|
||||||
|
<span class="inspector-badge">{{ task.language }}</span>
|
||||||
|
<span class="inspector-badge">{{ task.fileSize }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="percent !== null" class="progress-line">
|
||||||
|
<el-progress :percentage="percent" :stroke-width="5" :show-text="false" />
|
||||||
|
<span class="progress-value">{{ percent }}% · {{ task.proceeding }}/{{ task.pages }} 页</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="inspector-facts">
|
||||||
|
<dt>分辨率</dt>
|
||||||
|
<dd>{{ task.resolution || "—" }}</dd>
|
||||||
|
<dt>创建时间</dt>
|
||||||
|
<dd>{{ task.createTimeDisplay || "—" }}</dd>
|
||||||
|
<dt>文件大小</dt>
|
||||||
|
<dd>{{ task.fileSize || "—" }}</dd>
|
||||||
|
<dt>原始页面</dt>
|
||||||
|
<dd>
|
||||||
|
<a v-if="task.link" :href="task.link" target="_blank" rel="noopener">打开链接</a>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</dd>
|
||||||
|
<template v-if="isAdmin">
|
||||||
|
<dt>下载人</dt>
|
||||||
|
<dd>{{ task.downloaderName || `#${task.downloader}` }}</dd>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inspector-actions">
|
||||||
|
<el-button type="primary" :disabled="!isDone" @click="downloadTask">
|
||||||
|
<AppIcon name="download" :size="15" />下载
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="readOnline(task)">
|
||||||
|
<AppIcon name="eye" :size="15" />在线看
|
||||||
|
</el-button>
|
||||||
|
<el-button :class="{'is-collected': task.isCollect}" @click="toggleCollect">
|
||||||
|
<AppIcon name="star" :size="15" />{{ task.isCollect ? "已收藏" : "收藏" }}
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="!isDone" :loading="isRetrying" @click="retryTask">
|
||||||
|
<AppIcon name="refresh" :size="15" />重试
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" plain :disabled="!isDone" @click="deleteTask">
|
||||||
|
<AppIcon name="trash" :size="15" />删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else class="inspector-empty">
|
||||||
|
<AppIcon name="image" :size="26" />
|
||||||
|
<p>选择一条任务查看详情</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
|
|
||||||
import {ref, watch} from "vue";
|
|
||||||
import axios from "axios";
|
|
||||||
import {ElMessage} from "element-plus";
|
|
||||||
import store from "../store/index.js";
|
|
||||||
import {validateLink} from "../utils/validate.js";
|
|
||||||
|
|
||||||
let props = defineProps(['isQuerying'])
|
|
||||||
let emit = defineEmits(['close'])
|
|
||||||
let scrollBar = ref()
|
|
||||||
let keyword = ref("")
|
|
||||||
let queryPage = ref({})
|
|
||||||
let galleries = ref([])
|
|
||||||
let param = ref()
|
|
||||||
let isShowUp = ref()
|
|
||||||
let isLoading = ref(false)
|
|
||||||
|
|
||||||
watch(props, () => {
|
|
||||||
isShowUp.value = props.isQuerying
|
|
||||||
})
|
|
||||||
|
|
||||||
function queryGalleries(link) {
|
|
||||||
let tempParam
|
|
||||||
if (link !== null) {
|
|
||||||
let url = new URL(link)
|
|
||||||
tempParam = url.search.replace("?f_search=", "")
|
|
||||||
} else {
|
|
||||||
tempParam = keyword.value
|
|
||||||
}
|
|
||||||
tempParam = tempParam.replace(" ", "+")
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + tempParam)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.data.result === "success") {
|
|
||||||
let tempGalleries = JSON.parse(res.data.data)
|
|
||||||
queryPage.value.first = 'first' in res.data ? res.data.first : undefined
|
|
||||||
queryPage.value.previous = 'previous' in res.data ? res.data.previous : undefined
|
|
||||||
queryPage.value.next = 'next' in res.data ? res.data.next : undefined
|
|
||||||
queryPage.value.last = 'last' in res.data ? res.data.last : undefined
|
|
||||||
|
|
||||||
galleries.value.splice(0)
|
|
||||||
tempGalleries.forEach((gallery) => {
|
|
||||||
galleries.value.push(gallery)
|
|
||||||
})
|
|
||||||
|
|
||||||
scrollBar.value.setScrollTop(0)
|
|
||||||
} else {
|
|
||||||
ElMessage({message: res.data.data, type: "error"})
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
isLoading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryRemoteTask(){
|
|
||||||
if(!validateLink(param.value)){
|
|
||||||
ElMessage("链接错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.dispatch("queryGalleryTask", param.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(){
|
|
||||||
emit("close")
|
|
||||||
}
|
|
||||||
|
|
||||||
function thumbnailUrl(path) {
|
|
||||||
return "https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?" + new URLSearchParams({
|
|
||||||
path,
|
|
||||||
AuthCode: store.state.AuthCode
|
|
||||||
}).toString()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-dialog title="在线搜索" v-model="isShowUp" top="0" style="margin-bottom: 0" @close="close">
|
|
||||||
<div style="text-align: center">
|
|
||||||
<el-input v-model="keyword"></el-input> <el-button @click="queryGalleries(null)">查询</el-button> <div class="loading" v-show="isLoading"/>
|
|
||||||
</div>
|
|
||||||
<el-scrollbar height="75vh" ref="scrollBar">
|
|
||||||
<div style="height: 251px; width: 100%; " v-for="gallery in galleries">
|
|
||||||
<el-image alt="picture" :preview-src-list="[thumbnailUrl(gallery.thumbnailUrl)]"
|
|
||||||
:src="thumbnailUrl(gallery.thumbnailUrl)"
|
|
||||||
style="height:250px;width:250px;float: left;"
|
|
||||||
fit="contain"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div style="font: bold 16px semi-condensed; margin-top: 10px; padding-top: 15px; padding-left: 275px">
|
|
||||||
<span>{{gallery.name}}</span><br><br>
|
|
||||||
<span>上传时间:{{gallery.uploadTime}}</span><br>
|
|
||||||
<span>页数:{{gallery.page}}</span><br>
|
|
||||||
<span class="ct6">类型:{{gallery.type}}</span><br>
|
|
||||||
<a :href="gallery.link">链接</a><br>
|
|
||||||
<el-button-group style=" margin-left: 50%; margin-top: -5vh">
|
|
||||||
<el-button @click="store.dispatch('readOnlineGallery', gallery)">在线看</el-button>
|
|
||||||
<el-button @click="param=gallery.link; queryRemoteTask()">查看详情</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
<div style="padding-top: 1vh; text-align: center">
|
|
||||||
<el-button @click="queryGalleries(queryPage.first)" :disabled="queryPage.first === undefined">首页</el-button>
|
|
||||||
<el-button @click="queryGalleries(queryPage.previous)" :disabled="queryPage.previous === undefined">上一页</el-button>
|
|
||||||
<el-button @click="queryGalleries(queryPage.next)" :disabled="queryPage.next === undefined">下一页</el-button>
|
|
||||||
<el-button @click="queryGalleries(queryPage.last)" :disabled="queryPage.last === undefined">尾页</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.el-input{
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: inline-block;
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
border: 2px solid #ccc;
|
|
||||||
border-top-color: #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import AppIcon from "./AppIcon.vue";
|
||||||
|
|
||||||
|
const activeView = computed({
|
||||||
|
get: () => store.state.activeView,
|
||||||
|
set: value => store.commit("_setActiveView", value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskCount = computed(() => store.state.totalGalleryTask.length);
|
||||||
|
const searchCount = computed(() => store.state.searchTask.length);
|
||||||
|
|
||||||
|
const isConnected = computed(() => store.state.connectionStatus === "connected");
|
||||||
|
const connectionLabel = computed(() => ({
|
||||||
|
connected: "进度已连接",
|
||||||
|
connecting: "正在连接进度",
|
||||||
|
reconnecting: "连接中断,正在重连",
|
||||||
|
disconnected: "进度连接已断开",
|
||||||
|
})[store.state.connectionStatus]);
|
||||||
|
|
||||||
|
const username = computed(() => store.state.username || "未登录");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="nav-rail" aria-label="主导航">
|
||||||
|
<div class="nav-brand" aria-hidden="true">L</div>
|
||||||
|
|
||||||
|
<div class="nav-items">
|
||||||
|
<button type="button"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{'is-active': activeView === 'tasks'}"
|
||||||
|
:aria-current="activeView === 'tasks' ? 'page' : undefined"
|
||||||
|
title="任务"
|
||||||
|
@click="activeView = 'tasks'">
|
||||||
|
<AppIcon name="list" :size="20" />
|
||||||
|
<span v-if="taskCount" class="nav-badge">{{ taskCount }}</span>
|
||||||
|
<span class="nav-label">任务</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{'is-active': activeView === 'search'}"
|
||||||
|
:aria-current="activeView === 'search' ? 'page' : undefined"
|
||||||
|
title="搜索"
|
||||||
|
@click="activeView = 'search'">
|
||||||
|
<AppIcon name="search" :size="20" />
|
||||||
|
<span v-if="searchCount" class="nav-badge">{{ searchCount }}</span>
|
||||||
|
<span class="nav-label">搜索</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{'is-active': activeView === 'settings'}"
|
||||||
|
:aria-current="activeView === 'settings' ? 'page' : undefined"
|
||||||
|
title="设置"
|
||||||
|
@click="activeView = 'settings'">
|
||||||
|
<AppIcon name="settings" :size="20" />
|
||||||
|
<span class="nav-label">设置</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-foot">
|
||||||
|
<span class="nav-connection"
|
||||||
|
:class="{'is-connected': isConnected}"
|
||||||
|
role="status"
|
||||||
|
:title="connectionLabel">
|
||||||
|
<i class="connection-dot"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-user" :title="username">{{ username }}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
+415
-123
@@ -1,142 +1,434 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, ref, watch, onBeforeUnmount} from "vue";
|
import {computed, nextTick, onBeforeUnmount, ref, watch} from "vue";
|
||||||
import {startImages, settleImage, retryImage} from "../utils/progressiveImages.js";
|
import {startImages, settleImage, retryImage} from "../utils/progressiveImages.js";
|
||||||
import store from "../store/index.js";
|
import store from "../store/index.js";
|
||||||
let onlineReadingScrollbar = ref()
|
import AppIcon from "./AppIcon.vue";
|
||||||
let links = ref()
|
import {ehThumbnailUrl} from "../utils/thumbnail.js";
|
||||||
let index = ref(0) //下标
|
|
||||||
let page = ref(0) //页数 下标+1=页数 用于跳转
|
|
||||||
|
|
||||||
let imagesForLoading = ref([])
|
const viewer = ref(null);
|
||||||
let isReading = ref(false)
|
const readingGallery = computed(() => store.state.readingGallery);
|
||||||
|
|
||||||
let max = ref(0)
|
// Images arrive a page at a time; "page" is a window into the full gallery.
|
||||||
let current_page = 0
|
const links = ref([]);
|
||||||
let lengthPerPage = computed(() => {
|
const pageIndex = ref(0);
|
||||||
return Math.max(1, Number(store.state.lengthPerPage) || 10)
|
const pageInput = ref(1);
|
||||||
})
|
const isReading = ref(false);
|
||||||
let readingGallery = computed(() => {
|
const imagesForLoading = ref([]);
|
||||||
return store.state.readingGallery
|
const currentImage = ref(0);
|
||||||
})
|
const fitMode = ref("height");
|
||||||
watch(() => store.state.isReading, (isReadingVal) => {
|
|
||||||
if(isReadingVal && !isReading.value) {
|
// Remote galleries keep a raw thumbnail path; tasks store an already-proxied URL.
|
||||||
alterPage()
|
const thumbUrl = computed(() => {
|
||||||
isReading.value = true
|
const path = readingGallery.value.thumb_link;
|
||||||
|
if (!path) return "";
|
||||||
|
return /^https?:/i.test(path) ? path : ehThumbnailUrl(path, store.state.AuthCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lengthPerPage = computed(() => Math.max(1, Number(store.state.lengthPerPage) || 10));
|
||||||
|
const totalImages = computed(() => readingGallery.value.images.length);
|
||||||
|
const pageCount = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(totalImages.value / lengthPerPage.value)));
|
||||||
|
|
||||||
|
const rangeLabel = computed(() => {
|
||||||
|
if (totalImages.value <= lengthPerPage.value) return `共 ${totalImages.value} 页`;
|
||||||
|
const start = pageIndex.value * lengthPerPage.value + 1;
|
||||||
|
const end = Math.min(totalImages.value, start + links.value.length - 1);
|
||||||
|
return `${start} - ${end} / ${totalImages.value}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => store.state.isReading, open => {
|
||||||
|
if (open && !isReading.value) {
|
||||||
|
showPage(0);
|
||||||
|
isReading.value = true;
|
||||||
|
} else if (!open && isReading.value) {
|
||||||
|
isReading.value = false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
//切换
|
function showPage(target) {
|
||||||
function alterPage(){
|
if (target < 0 || target >= pageCount.value) return;
|
||||||
if(readingGallery.value.images.length > lengthPerPage.value){
|
pageIndex.value = target;
|
||||||
links.value = readingGallery.value.images.slice(0, lengthPerPage.value)
|
pageInput.value = target + 1;
|
||||||
max.value = Math.ceil(readingGallery.value.images.length / lengthPerPage.value)
|
currentImage.value = 0;
|
||||||
}else{
|
links.value = readingGallery.value.images.slice(
|
||||||
links.value = readingGallery.value.images
|
target * lengthPerPage.value,
|
||||||
max.value = 0
|
(target + 1) * lengthPerPage.value
|
||||||
|
);
|
||||||
|
imagesForLoading.value = startImages(links.value);
|
||||||
|
nextTick(() => viewer.value?.setScrollTop(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageResult(item, failed = false) {
|
||||||
|
settleImage(imagesForLoading.value, links.value, item, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
imagesForLoading.value = [];
|
||||||
|
links.value = [];
|
||||||
|
store.commit("_closeReader");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll the active image into view when the user steps between images.
|
||||||
|
function revealCurrent() {
|
||||||
|
nextTick(() => {
|
||||||
|
const node = viewer.value?.wrapRef?.querySelectorAll(".reader-image")[currentImage.value];
|
||||||
|
node?.scrollIntoView({block: "nearest", behavior: "smooth"});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function step(delta) {
|
||||||
|
if (currentImage.value + delta < 0) {
|
||||||
|
if (pageIndex.value > 0) showPage(pageIndex.value - 1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
index.value = 0
|
if (currentImage.value + delta >= links.value.length) {
|
||||||
page.value = 1
|
if (pageIndex.value < pageCount.value - 1) showPage(pageIndex.value + 1);
|
||||||
imagesForLoading.value = startImages(links.value)
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
//跳转到对应页数
|
|
||||||
function jump(targetIndex){
|
|
||||||
if(targetIndex < 0 || targetIndex >= Math.ceil(readingGallery.value.images.length / lengthPerPage.value)) return
|
|
||||||
clearPageSwitchTimer()
|
|
||||||
links.value = readingGallery.value.images.slice(targetIndex * lengthPerPage.value, (targetIndex + 1) * lengthPerPage.value)
|
|
||||||
index.value = targetIndex
|
|
||||||
page.value = targetIndex + 1
|
|
||||||
imagesForLoading.value = startImages(links.value)
|
|
||||||
onlineReadingScrollbar.value.setScrollTop(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function imageResult(item, failed = false){
|
|
||||||
settleImage(imagesForLoading.value, links.value, item, failed)
|
|
||||||
}
|
|
||||||
|
|
||||||
let pageSwitchTimer
|
|
||||||
function clearPageSwitchTimer(){
|
|
||||||
clearInterval(pageSwitchTimer)
|
|
||||||
pageSwitchTimer = undefined
|
|
||||||
}
|
|
||||||
onBeforeUnmount(clearPageSwitchTimer)
|
|
||||||
|
|
||||||
function closeDialog(){
|
|
||||||
clearPageSwitchTimer()
|
|
||||||
imagesForLoading.value = []
|
|
||||||
onlineReadingScrollbar.value.setScrollTop(0)
|
|
||||||
isReading.value = false
|
|
||||||
store.commit("_closeReader")
|
|
||||||
}
|
|
||||||
|
|
||||||
function switch_page(target_page){
|
|
||||||
//上一页
|
|
||||||
if(target_page > (current_page + 1) && current_page === 0 && index.value > 0) {
|
|
||||||
document.querySelector("span.el-image-viewer__btn.el-image-viewer__close").click()
|
|
||||||
jump(index.value - 1)
|
|
||||||
onlineReadingScrollbar.value.setScrollTop(onlineReadingScrollbar.value.wrapRef.scrollHeight)
|
|
||||||
let top = 0
|
|
||||||
pageSwitchTimer = setInterval(() => {
|
|
||||||
if(onlineReadingScrollbar.value.scrollTop === top){
|
|
||||||
clearPageSwitchTimer()
|
|
||||||
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:last-child > img").click()
|
|
||||||
}
|
|
||||||
top = onlineReadingScrollbar.value.scrollTop
|
|
||||||
onlineReadingScrollbar.value.setScrollTop(onlineReadingScrollbar.value.wrapRef.scrollHeight)
|
|
||||||
}, 100)
|
|
||||||
//下一页
|
|
||||||
}else if(target_page === 0 && current_page === lengthPerPage.value - 1 && index.value < max.value - 1){
|
|
||||||
jump(index.value + 1)
|
|
||||||
current_page = 0
|
|
||||||
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:nth-child(1) > img").click()
|
|
||||||
}
|
}
|
||||||
else{
|
currentImage.value += delta;
|
||||||
current_page = target_page
|
revealCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader shortcuts: arrows step, space/esc close or advance, +/- fit.
|
||||||
|
function onKeydown(event) {
|
||||||
|
if (!isReading.value) return;
|
||||||
|
// The built-in image viewer owns the keyboard while it is open.
|
||||||
|
if (document.querySelector(".el-image-viewer__wrapper")) return;
|
||||||
|
const key = event.key;
|
||||||
|
if (key === "ArrowRight" || key === "PageDown" || key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
step(1);
|
||||||
|
} else if (key === "ArrowLeft" || key === "PageUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
step(-1);
|
||||||
|
} else if (key === "Escape") {
|
||||||
|
close();
|
||||||
|
} else if (key === "+" || key === "=") {
|
||||||
|
fitMode.value = "height";
|
||||||
|
} else if (key === "-") {
|
||||||
|
fitMode.value = "width";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_current_page(page){
|
window.addEventListener("keydown", onKeydown);
|
||||||
current_page = page
|
onBeforeUnmount(() => {
|
||||||
}
|
window.removeEventListener("keydown", onKeydown);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="isReading" width="90%" style="margin-top: 0; margin-bottom: 0; padding: 0" @close="closeDialog">
|
<Teleport to="body">
|
||||||
<template #header style="padding-bottom: 0">
|
<div v-if="isReading" class="reader-layer" role="dialog" aria-modal="true" aria-label="在线预览">
|
||||||
在线预览: {{readingGallery.name}}<br>
|
<header class="reader-bar">
|
||||||
页数:{{readingGallery.pages}}<br>
|
<span class="reader-thumb">
|
||||||
<div style="font-size: 3vh; display: inline" v-if="max > 0">
|
<img v-if="thumbUrl" :src="thumbUrl" alt="">
|
||||||
{{ index + 1 }} - {{ (index) * lengthPerPage + 1 }} ~
|
<AppIcon v-else name="image" :size="16" />
|
||||||
{{ (index + 1) * lengthPerPage + 1 > readingGallery.images.length ? readingGallery.images.length : (index + 1) * lengthPerPage }}
|
</span>
|
||||||
</div>
|
<div class="reader-heading">
|
||||||
</template>
|
<p class="reader-title" :title="readingGallery.name">{{ readingGallery.name }}</p>
|
||||||
<el-scrollbar height="75vh" ref="onlineReadingScrollbar">
|
<p class="reader-sub">
|
||||||
<div v-for="(item, i) in imagesForLoading" :key="item.url + '-' + item.attempt" style="display: inline-block; text-align: center; min-height: 300px">
|
<span>{{ rangeLabel }}</span>
|
||||||
<el-image :src="item.src" :style="{'width': '66vw', 'background-color': 'gary'}"
|
<span v-if="readingGallery.language">{{ readingGallery.language }}</span>
|
||||||
:preview-src-list="links" :initial-index="i" @switch="switch_page" @show="set_current_page(i)" loading="lazy" @load="imageResult(item)" @error="imageResult(item, true)"/>
|
<span v-if="readingGallery.fileSize">{{ readingGallery.fileSize }}</span>
|
||||||
<div v-if="item.failed" role="alert">图片加载失败 <el-button size="small" @click="retryImage(item)">重试</el-button></div><br>
|
</p>
|
||||||
{{lengthPerPage * index + i + 1}}
|
</div>
|
||||||
</div>
|
<div class="reader-tools">
|
||||||
</el-scrollbar>
|
<div class="segmented is-dark" role="group" aria-label="缩放模式">
|
||||||
|
<button type="button" :aria-pressed="fitMode === 'height'" @click="fitMode = 'height'">适应高度</button>
|
||||||
|
<button type="button" :aria-pressed="fitMode === 'width'" @click="fitMode = 'width'">适应宽度</button>
|
||||||
|
</div>
|
||||||
|
<a v-if="readingGallery.link" class="icon-button reader-external" title="打开原始页面"
|
||||||
|
aria-label="打开原始页面" :href="readingGallery.link" target="_blank" rel="noopener">
|
||||||
|
<AppIcon name="external" :size="15" />
|
||||||
|
</a>
|
||||||
|
<button type="button" class="icon-button" title="关闭" aria-label="关闭" @click="close">
|
||||||
|
<AppIcon name="close" :size="15" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- 十页以下-->
|
<el-scrollbar class="reader-scroll" ref="viewer">
|
||||||
<span v-if="max > 1 && max < 9">
|
<div class="reader-stack" :class="`is-${fitMode}`">
|
||||||
<el-button @click="jump(index - 1)" :disabled="index === 0">上一页</el-button>
|
<figure class="reader-image" v-for="(item, i) in imagesForLoading" :key="item.url + '-' + item.attempt">
|
||||||
<el-button v-for="i in max" @click="jump(i - 1)">
|
<el-image :src="item.src"
|
||||||
{{i}}
|
:preview-src-list="links"
|
||||||
</el-button>
|
:initial-index="i"
|
||||||
<el-button @click="jump(index + 1)" :disabled="index === max - 1">下一页</el-button>
|
fit="contain"
|
||||||
</span>
|
loading="lazy"
|
||||||
|
@show="currentImage = i"
|
||||||
|
@load="imageResult(item)"
|
||||||
|
@error="imageResult(item, true)" />
|
||||||
|
<div v-if="item.failed" class="reader-error" role="alert">
|
||||||
|
图片加载失败
|
||||||
|
<el-button size="small" @click="retryImage(item)">重试</el-button>
|
||||||
|
</div>
|
||||||
|
<figcaption>{{ pageIndex * lengthPerPage + i + 1 }}</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
<!-- 十页及以上-->
|
<footer class="reader-foot">
|
||||||
<span v-if="max >= 9">
|
<button type="button" class="text-button" :disabled="pageIndex === 0" @click="showPage(pageIndex - 1)">
|
||||||
<el-button @click="jump(index - 1)" :disabled="index === 0">上一页</el-button>
|
<AppIcon name="chevrons-left" :size="15" />上一组
|
||||||
1 <
|
</button>
|
||||||
<el-input-number v-model="page" :min="1" :max="max"/>
|
<button type="button" class="text-button" :disabled="currentImage === 0" @click="step(-1)">
|
||||||
< {{max}}
|
<AppIcon name="chevron-left" :size="15" />上一张
|
||||||
<el-button @click="jump(index + 1)" :disabled="index === max - 1">下一页</el-button>
|
</button>
|
||||||
<el-button @click="jump(page - 1)" size="large">跳转</el-button>
|
|
||||||
</span>
|
<span class="reader-pager">
|
||||||
</el-dialog>
|
<el-input-number v-model="pageInput"
|
||||||
|
:min="1"
|
||||||
|
:max="pageCount"
|
||||||
|
size="small"
|
||||||
|
controls-position="right"
|
||||||
|
@change="showPage(Number(pageInput) - 1)" />
|
||||||
|
<span class="reader-page-total">/ {{ pageCount }} 组</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button type="button" class="text-button"
|
||||||
|
:disabled="currentImage >= links.length - 1"
|
||||||
|
@click="step(1)">
|
||||||
|
下一张<AppIcon name="chevron-right" :size="15" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="text-button"
|
||||||
|
:disabled="pageIndex >= pageCount - 1"
|
||||||
|
@click="showPage(pageIndex + 1)">
|
||||||
|
下一组<AppIcon name="chevrons-right" :size="15" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="reader-foot-spacer"></span>
|
||||||
|
<span class="reader-hint">{{ currentImage + 1 }} / {{ links.length }}</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reader-layer {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #0f1116;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-bar {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #171a21;
|
||||||
|
border-bottom: 1px solid #262a33;
|
||||||
|
color: #e8eaee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-thumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #22262f;
|
||||||
|
color: #6b7280;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-heading {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e8eaee;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sub {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 14px;
|
||||||
|
margin: 3px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa3b2;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-tools .icon-button {
|
||||||
|
background: #1e222a;
|
||||||
|
border-color: #2f3542;
|
||||||
|
color: #c6ccd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-tools .icon-button:hover {
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The segmented control needs a dark variant inside the reader chrome. */
|
||||||
|
.reader-tools .segmented.is-dark {
|
||||||
|
background: #1e222a;
|
||||||
|
border-color: #2f3542;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-tools .segmented.is-dark button {
|
||||||
|
color: #9aa3b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-tools .segmented.is-dark button[aria-pressed="true"] {
|
||||||
|
background: #2f3542;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-external {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-image {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-image :deep(.el-image) {
|
||||||
|
background: #171a21;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-stack.is-height .reader-image :deep(.el-image) {
|
||||||
|
height: calc(100vh - 168px);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-stack.is-width .reader-image :deep(.el-image) {
|
||||||
|
width: min(1100px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-stack.is-width .reader-image :deep(.el-image img) {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-error {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-image figcaption {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-foot {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #171a21;
|
||||||
|
border-top: 1px solid #262a33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-foot .text-button {
|
||||||
|
background: #1e222a;
|
||||||
|
border-color: #2f3542;
|
||||||
|
color: #c6ccd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-foot .text-button:hover:not(:disabled) {
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-foot .text-button:disabled {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-pager {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-pager :deep(.el-input-number) {
|
||||||
|
width: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the group picker in the dark palette used by the reader chrome. */
|
||||||
|
.reader-pager :deep(.el-input__wrapper) {
|
||||||
|
background: #1e222a;
|
||||||
|
box-shadow: 0 0 0 1px #2f3542 inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-pager :deep(.el-input__inner) {
|
||||||
|
color: #e8eaee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-pager :deep(.el-input-number__increase),
|
||||||
|
.reader-pager :deep(.el-input-number__decrease) {
|
||||||
|
background: #262a33;
|
||||||
|
border-color: #2f3542;
|
||||||
|
color: #c6ccd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-pager :deep(.el-input-number__increase:hover),
|
||||||
|
.reader-pager :deep(.el-input-number__decrease:hover) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-page-total {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9aa3b2;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-foot-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa3b2;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import TaskRow from "./TaskRow.vue";
|
||||||
|
import AppIcon from "./AppIcon.vue";
|
||||||
|
import {validateLink} from "../utils/validate.js";
|
||||||
|
import {ehThumbnailUrl} from "../utils/thumbnail.js";
|
||||||
|
|
||||||
|
const mode = ref("ehentai");
|
||||||
|
const localMode = ref("keyword");
|
||||||
|
const localKeyword = ref("");
|
||||||
|
const linkParam = ref("");
|
||||||
|
|
||||||
|
const remoteKeyword = ref("");
|
||||||
|
const remoteGalleries = ref([]);
|
||||||
|
const remotePage = ref({});
|
||||||
|
const remoteLoading = ref(false);
|
||||||
|
const retryingGids = ref(new Set());
|
||||||
|
|
||||||
|
const localResults = computed(() => store.getters.currentTasks || []);
|
||||||
|
const localTotal = computed(() => store.state.searchTask.length);
|
||||||
|
const isSearching = computed(() => store.state.isSearch);
|
||||||
|
|
||||||
|
function runLocalSearch() {
|
||||||
|
if (localKeyword.value.trim() === "") {
|
||||||
|
ElMessage("请输入查询内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (localMode.value === "link") store.commit("_searchLocalByLink", localKeyword.value);
|
||||||
|
else store.commit("_searchLocalByKeyword", localKeyword.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLocalSearch() {
|
||||||
|
store.commit("_searchLocalByKeyword", "");
|
||||||
|
localKeyword.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryGalleries(link) {
|
||||||
|
let keyword;
|
||||||
|
if (link != null) {
|
||||||
|
keyword = new URL(link).search.replace("?f_search=", "");
|
||||||
|
} else {
|
||||||
|
keyword = remoteKeyword.value;
|
||||||
|
if (keyword.trim() === "") {
|
||||||
|
ElMessage("请输入搜索关键词");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remoteLoading.value = true;
|
||||||
|
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + keyword.replace(" ", "+"))
|
||||||
|
.then(res => {
|
||||||
|
if (res.data.result === "success") {
|
||||||
|
remotePage.value = {
|
||||||
|
first: res.data.first,
|
||||||
|
previous: res.data.previous,
|
||||||
|
next: res.data.next,
|
||||||
|
last: res.data.last,
|
||||||
|
};
|
||||||
|
remoteGalleries.value.splice(0);
|
||||||
|
JSON.parse(res.data.data).forEach(g => remoteGalleries.value.push(g));
|
||||||
|
} else {
|
||||||
|
ElMessage({message: res.data.data, type: "error"});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
remoteLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand a remote gallery to the shared resolver so its resolution can be picked.
|
||||||
|
function submitRemote(gallery) {
|
||||||
|
store.dispatch("queryGalleryTask", gallery.link);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview a remote gallery without submitting it first.
|
||||||
|
function previewRemote(gallery) {
|
||||||
|
store.dispatch("readOnlineGallery", gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLink() {
|
||||||
|
let link = linkParam.value;
|
||||||
|
if (!validateLink(link)) {
|
||||||
|
ElMessage("链接错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (link.includes("e-hentai")) link = link.replace("e-hentai", "exhentai");
|
||||||
|
store.dispatch("queryGalleryTask", link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbFor(path) {
|
||||||
|
return ehThumbnailUrl(path, store.state.AuthCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTask(gallery) {
|
||||||
|
if (gallery.download) window.open(gallery.download);
|
||||||
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryGallery(gallery) {
|
||||||
|
if (retryingGids.value.has(gallery.gid)) return;
|
||||||
|
retryingGids.value.add(gallery.gid);
|
||||||
|
try {
|
||||||
|
await store.dispatch("retryGallery", gallery.gid);
|
||||||
|
} finally {
|
||||||
|
retryingGids.value.delete(gallery.gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view">
|
||||||
|
<header class="view-bar">
|
||||||
|
<h1 class="view-title">搜索</h1>
|
||||||
|
<span class="view-count" v-if="mode === 'ehentai' && remoteGalleries.length">本页 {{ remoteGalleries.length }} 个结果</span>
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<div class="segmented is-quiet" role="group" aria-label="搜索来源">
|
||||||
|
<button type="button" :aria-pressed="mode === 'ehentai'" @click="mode = 'ehentai'">E站搜索</button>
|
||||||
|
<button type="button" :aria-pressed="mode === 'local'" @click="mode = 'local'">本地任务</button>
|
||||||
|
<button type="button" :aria-pressed="mode === 'link'" @click="mode = 'link'">解析链接</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-scroll">
|
||||||
|
<template v-if="mode === 'ehentai'">
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input v-model="remoteKeyword"
|
||||||
|
placeholder="在 E站搜索画廊"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="queryGalleries(null)" />
|
||||||
|
<el-button type="primary" :loading="remoteLoading" @click="queryGalleries(null)">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="remote-row" v-for="gallery in remoteGalleries" :key="gallery.link">
|
||||||
|
<span class="task-thumb">
|
||||||
|
<img :src="thumbFor(gallery.thumbnailUrl)" alt="" loading="lazy">
|
||||||
|
</span>
|
||||||
|
<div class="task-body">
|
||||||
|
<p class="task-title" :title="gallery.name">{{ gallery.name }}</p>
|
||||||
|
<p class="task-meta">
|
||||||
|
<span>{{ gallery.page }} 页</span>
|
||||||
|
<span>{{ gallery.type }}</span>
|
||||||
|
<span>{{ (gallery.uploadTime || "").split(" ")[0] }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="task-acts">
|
||||||
|
<button type="button" class="text-button" @click="previewRemote(gallery)">
|
||||||
|
<AppIcon name="eye" :size="15" />在线看
|
||||||
|
</button>
|
||||||
|
<el-button size="small" type="primary" @click="submitRemote(gallery)">提交</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!remoteGalleries.length && !remoteLoading" class="empty-state">输入关键词开始搜索</p>
|
||||||
|
|
||||||
|
<div class="pager-row" v-if="remoteGalleries.length">
|
||||||
|
<el-button size="small" :disabled="remotePage.first === undefined" @click="queryGalleries(remotePage.first)">首页</el-button>
|
||||||
|
<el-button size="small" :disabled="remotePage.previous === undefined" @click="queryGalleries(remotePage.previous)">上一页</el-button>
|
||||||
|
<el-button size="small" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.next)">下一页</el-button>
|
||||||
|
<el-button size="small" :disabled="remotePage.last === undefined" @click="queryGalleries(remotePage.last)">尾页</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="mode === 'local'">
|
||||||
|
<div class="search-bar">
|
||||||
|
<div class="segmented is-quiet" role="group" aria-label="本地查询方式">
|
||||||
|
<button type="button" :aria-pressed="localMode === 'keyword'" @click="localMode = 'keyword'">关键字</button>
|
||||||
|
<button type="button" :aria-pressed="localMode === 'link'" @click="localMode = 'link'">链接</button>
|
||||||
|
</div>
|
||||||
|
<el-input v-model="localKeyword"
|
||||||
|
:placeholder="localMode === 'keyword' ? '在我的下载里搜索' : '粘贴已下载任务的链接'"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="runLocalSearch" />
|
||||||
|
<el-button type="primary" @click="runLocalSearch">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isSearching">
|
||||||
|
<p class="hint">
|
||||||
|
匹配到 {{ localTotal }} 项
|
||||||
|
<button type="button" class="hint-action" @click="clearLocalSearch">清除</button>
|
||||||
|
</p>
|
||||||
|
<TaskRow v-for="gallery in localResults"
|
||||||
|
:key="gallery.gid"
|
||||||
|
:gallery="gallery"
|
||||||
|
:retrying="retryingGids.has(gallery.gid)"
|
||||||
|
@open="store.commit('_selectGallery', $event)"
|
||||||
|
@download="downloadTask"
|
||||||
|
@retry="retryGallery" />
|
||||||
|
</template>
|
||||||
|
<p v-else class="empty-state">输入关键字或链接在已有任务里查找</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input v-model="linkParam"
|
||||||
|
placeholder="粘贴 e-hentai 画廊链接"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="parseLink" />
|
||||||
|
<el-button type="primary" @click="parseLink">解析</el-button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">解析成功后可直接选分辨率提交下载</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, onMounted, ref} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import AppIcon from "./AppIcon.vue";
|
||||||
|
|
||||||
|
const weekUsed = computed(() => store.state.weekUsed);
|
||||||
|
const isAdmin = computed(() => store.state.isAdmin);
|
||||||
|
const username = computed(() => store.state.username || "—");
|
||||||
|
|
||||||
|
const isDark = ref(document.documentElement.classList.contains("dark"));
|
||||||
|
const followSystem = ref(false);
|
||||||
|
const lengthPerPage = ref(30);
|
||||||
|
|
||||||
|
const category = computed({
|
||||||
|
get: () => store.state.category,
|
||||||
|
set: value => {
|
||||||
|
store.commit("_setCategory", value);
|
||||||
|
localStorage.setItem("category", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const sortType = computed({
|
||||||
|
get: () => store.state.sortType,
|
||||||
|
set: value => {
|
||||||
|
store.commit("_setSortType", value);
|
||||||
|
localStorage.setItem("sortType", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const galleryNameType = computed({
|
||||||
|
get: () => store.state.galleryNameType,
|
||||||
|
set: value => {
|
||||||
|
store.commit("_setGalleryNameType", value);
|
||||||
|
localStorage.setItem("galleryNameType", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auth code editing stays in a dialog: it is a destructive, infrequent action.
|
||||||
|
const isAlterAuthCode = ref(false);
|
||||||
|
const newAuthCode = ref("");
|
||||||
|
const tempAuthCode = ref("");
|
||||||
|
const realAuthCode = computed(() => store.state.AuthCode);
|
||||||
|
|
||||||
|
function applyTheme() {
|
||||||
|
localStorage.setItem("darkConfig", JSON.stringify({followSystem: followSystem.value}));
|
||||||
|
const dark = followSystem.value
|
||||||
|
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
: isDark.value;
|
||||||
|
document.documentElement.classList.toggle("dark", dark);
|
||||||
|
isDark.value = dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDark(value) {
|
||||||
|
followSystem.value = false;
|
||||||
|
isDark.value = value;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFollowSystem(value) {
|
||||||
|
followSystem.value = value;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLengthPerPage(value) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 30) {
|
||||||
|
ElMessage("分页页数设置错误,范围 1~30");
|
||||||
|
lengthPerPage.value = store.state.lengthPerPage || 30;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.state.lengthPerPage = parsed;
|
||||||
|
localStorage.setItem("lengthPerPage", String(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function alterAuthCode() {
|
||||||
|
if (newAuthCode.value.trim() === "" || newAuthCode.value !== tempAuthCode.value) {
|
||||||
|
ElMessage("请检查授权码输入是否错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch("alterAuthCode", newAuthCode.value);
|
||||||
|
isAlterAuthCode.value = false;
|
||||||
|
newAuthCode.value = "";
|
||||||
|
tempAuthCode.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAuthCode() {
|
||||||
|
localStorage.removeItem("auth");
|
||||||
|
ElMessage("删除授权码完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnect() {
|
||||||
|
store.dispatch("reconnect");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUndone() {
|
||||||
|
store.dispatch("resetUndone").then();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUsage() {
|
||||||
|
store.dispatch("loadWeekUsedAmount");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const darkConfigStr = localStorage.getItem("darkConfig");
|
||||||
|
if (darkConfigStr !== null) {
|
||||||
|
try {
|
||||||
|
followSystem.value = Boolean(JSON.parse(darkConfigStr).followSystem);
|
||||||
|
} catch {
|
||||||
|
followSystem.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lengthPerPage.value = Number(store.state.lengthPerPage) || 30;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view">
|
||||||
|
<header class="view-bar">
|
||||||
|
<h1 class="view-title">设置</h1>
|
||||||
|
<span class="view-count">{{ username }}</span>
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="settings-scroll">
|
||||||
|
<section class="settings-group">
|
||||||
|
<h2 class="section-title">本周用量</h2>
|
||||||
|
<div class="usage-line">
|
||||||
|
<span class="usage-value">{{ weekUsed.weekUsedAmount ?? "—" }}</span>
|
||||||
|
<span class="usage-note">上次重置 {{ weekUsed.lastResetAmountTime ?? "—" }}</span>
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
<button type="button" class="text-button" @click="refreshUsage">
|
||||||
|
<AppIcon name="refresh" :size="15" />刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-group">
|
||||||
|
<h2 class="section-title">外观</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>夜间模式</span>
|
||||||
|
<el-switch :model-value="isDark" @update:model-value="toggleDark" />
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>跟随系统</span>
|
||||||
|
<el-switch :model-value="followSystem" @update:model-value="toggleFollowSystem" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-group">
|
||||||
|
<h2 class="section-title">默认视图</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>分类</span>
|
||||||
|
<el-select v-model="category" style="width: 180px">
|
||||||
|
<el-option label="我的下载" value="myDownload" />
|
||||||
|
<el-option label="我的收藏" value="myCollect" />
|
||||||
|
<el-option label="全部" value="total" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>排序方式</span>
|
||||||
|
<el-select v-model="sortType" style="width: 180px">
|
||||||
|
<el-option label="简洁名字" value="shortName" />
|
||||||
|
<el-option label="名字" value="name" />
|
||||||
|
<el-option label="任务创建时间" value="createTime" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>显示类型</span>
|
||||||
|
<el-select v-model="galleryNameType" style="width: 180px">
|
||||||
|
<el-option label="简洁名字" value="shortName" />
|
||||||
|
<el-option label="名字" value="name" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>在线预览每页图片数量</span>
|
||||||
|
<el-input-number :model-value="lengthPerPage"
|
||||||
|
:min="1"
|
||||||
|
:max="30"
|
||||||
|
:step="1"
|
||||||
|
style="width: 160px"
|
||||||
|
@change="saveLengthPerPage" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-group">
|
||||||
|
<h2 class="section-title">授权</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>当前授权码</span>
|
||||||
|
<span class="mono-value">{{ realAuthCode }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>管理授权码</span>
|
||||||
|
<span class="setting-actions">
|
||||||
|
<el-button size="small" @click="isAlterAuthCode = true">修改授权码</el-button>
|
||||||
|
<el-button size="small" @click="deleteAuthCode">删除本地授权码</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-group">
|
||||||
|
<h2 class="section-title">系统</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>下载节点</span>
|
||||||
|
<span class="setting-actions">
|
||||||
|
<el-button size="small" @click="reconnect">重连节点</el-button>
|
||||||
|
<el-button v-if="isAdmin" size="small" type="danger" plain @click="resetUndone">重置任务</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog title="修改授权码" v-model="isAlterAuthCode" width="420px">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="当前授权码">
|
||||||
|
<span class="mono-value">{{ realAuthCode }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新的授权码">
|
||||||
|
<el-input v-model="newAuthCode" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="再次输入授权码">
|
||||||
|
<el-input v-model="tempAuthCode" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="isAlterAuthCode = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="alterAuthCode">提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="side-table-wrapper">
|
|
||||||
<div v-show="loadComplete" class="load_complete">
|
|
||||||
<div class="panel-head">
|
|
||||||
<h2 class="section-title">任务</h2>
|
|
||||||
<span class="head-meta">{{ currentTasks.length }} 项</span>
|
|
||||||
<span class="panel-head-spacer"></span>
|
|
||||||
<el-tag :type="connectionTagType" size="small" effect="light" role="status">
|
|
||||||
{{ connectionText }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-scroll">
|
|
||||||
<el-table :data="currentTasks"
|
|
||||||
:empty-text="emptyText"
|
|
||||||
:row-key="gallery=>gallery.gid"
|
|
||||||
class="task-table"
|
|
||||||
:max-height="tableMaxHeight"
|
|
||||||
@cellMouseEnter="showThumbnail"
|
|
||||||
>
|
|
||||||
|
|
||||||
<el-table-column type="expand" width="40">
|
|
||||||
<template #default="props">
|
|
||||||
<dl class="detail-info">
|
|
||||||
<dt>名字</dt>
|
|
||||||
<dd>{{ props.row.name }}</dd>
|
|
||||||
<dt>链接</dt>
|
|
||||||
<dd><el-link :href="props.row.link" target="_blank" rel="noopener">打开原始页面</el-link></dd>
|
|
||||||
<dt>语言</dt>
|
|
||||||
<dd>{{ props.row.language }}</dd>
|
|
||||||
<dt>页数</dt>
|
|
||||||
<dd>{{ props.row.pages }}</dd>
|
|
||||||
<dt>文件大小</dt>
|
|
||||||
<dd>{{ props.row.fileSize }}</dd>
|
|
||||||
<dt>分辨率</dt>
|
|
||||||
<dd>{{ props.row.resolution }}</dd>
|
|
||||||
<dt>创建时间</dt>
|
|
||||||
<dd>{{ props.row.createTimeDisplay }}</dd>
|
|
||||||
<template v-if="isLion">
|
|
||||||
<dt>downloader</dt>
|
|
||||||
<dd>{{ props.row.downloader }}</dd>
|
|
||||||
</template>
|
|
||||||
<div class="detail-actions">
|
|
||||||
<el-button size="small"
|
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
:icon="TrashIcon"
|
|
||||||
@click="deleteGallery(props.row.gid)"
|
|
||||||
:disabled="props.row.status !== '下载完成'">删除任务</el-button>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column label="名字" min-width="220">
|
|
||||||
<template #default="scoped">
|
|
||||||
<span class="task-name">
|
|
||||||
{{galleryNameType === 'shortName' ? scoped.row.shortName : scoped.row.name}}
|
|
||||||
</span>
|
|
||||||
<span class="task-size">{{ scoped.row.fileSize }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column label="状态" width="176">
|
|
||||||
<template #default="scoped">
|
|
||||||
<div class="state-cell">
|
|
||||||
<span class="status-pill" :class="statusClass(scoped.row.status)">{{ scoped.row.status }}</span>
|
|
||||||
<template v-if="progressPercent(scoped.row) !== null">
|
|
||||||
<el-progress :percentage="progressPercent(scoped.row)"
|
|
||||||
:stroke-width="5"
|
|
||||||
:show-text="false" />
|
|
||||||
<span class="progress-value">{{ progressLabel(scoped.row) }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column label="操作" width="168" align="right">
|
|
||||||
<template #default="scoped">
|
|
||||||
<div class="row-actions">
|
|
||||||
<el-tooltip content="下载文件" placement="top" :show-after="400">
|
|
||||||
<el-button size="small"
|
|
||||||
type="primary"
|
|
||||||
:icon="DownloadIcon"
|
|
||||||
:disabled="scoped.row.status !== '下载完成'"
|
|
||||||
@click="downloadTask(scoped.row.download)"
|
|
||||||
aria-label="下载文件"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip content="在线看" placement="top" :show-after="400">
|
|
||||||
<el-button size="small"
|
|
||||||
:icon="EyeIcon"
|
|
||||||
@click="readOnlineGallery(scoped.row)"
|
|
||||||
aria-label="在线看"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip :content="scoped.row.isCollect ? '取消收藏' : '收藏'"
|
|
||||||
placement="top"
|
|
||||||
:show-after="400">
|
|
||||||
<el-button size="small"
|
|
||||||
:icon="StarIcon"
|
|
||||||
:class="{ 'is-collected': scoped.row.isCollect }"
|
|
||||||
@click="changeGalleryCollect(scoped.row.gid, scoped.row.isCollect)"
|
|
||||||
:aria-label="scoped.row.isCollect ? '取消收藏' : '收藏'"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip content="重试任务" placement="top" :show-after="400">
|
|
||||||
<el-button v-if="scoped.row.status !== '下载完成'"
|
|
||||||
size="small"
|
|
||||||
:icon="RefreshIcon"
|
|
||||||
:loading="retryingGids.has(scoped.row.gid)"
|
|
||||||
@click="retryGallery(scoped.row.gid)"
|
|
||||||
aria-label="重试任务"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="list-toolbar">
|
|
||||||
<template v-if="username !== 'test'">
|
|
||||||
<el-select v-model="category" @change="changeCategory" style="width: 128px">
|
|
||||||
<template #prefix>分类</template>
|
|
||||||
<el-option value="myCollect" label="我的收藏"/>
|
|
||||||
<el-option value="myDownload" label="我的下载"/>
|
|
||||||
<el-option value="total" label="全部"/>
|
|
||||||
</el-select>
|
|
||||||
<el-select v-model="sortType" @change="changeSortType" style="width: 150px">
|
|
||||||
<template #prefix>排序</template>
|
|
||||||
<el-option value="name" label="名字"/>
|
|
||||||
<el-option value="shortName" label="简洁名字"/>
|
|
||||||
<el-option value="createTime" label="任务创建时间"/>
|
|
||||||
</el-select>
|
|
||||||
<el-select v-model="galleryNameType" @change="changeGalleryNameType" style="width: 132px">
|
|
||||||
<template #prefix>显示</template>
|
|
||||||
<el-option value="name" label="名字"/>
|
|
||||||
<el-option value="shortName" label="简洁名字"/>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<span class="toolbar-spacer"></span>
|
|
||||||
|
|
||||||
<div class="pager">
|
|
||||||
<el-button size="small" :icon="FirstIcon" :disabled="page <= min" @click="toMin" aria-label="第一页"></el-button>
|
|
||||||
<el-button size="small" :icon="PrevIcon" :disabled="page <= min" @click="previous" aria-label="上一页"></el-button>
|
|
||||||
<span class="pager-readout">
|
|
||||||
<el-input v-model="targetPage"
|
|
||||||
@change="changePage"
|
|
||||||
v-show="isEditingPage"
|
|
||||||
@blur="reverseEditMode"
|
|
||||||
class="page-input"
|
|
||||||
size="small"
|
|
||||||
ref="inputNode"></el-input>
|
|
||||||
<button v-show="!isEditingPage"
|
|
||||||
type="button"
|
|
||||||
class="page-current"
|
|
||||||
@click="reverseEditMode">{{ page }}</button>
|
|
||||||
<span class="page-total">/ {{ max }}</span>
|
|
||||||
</span>
|
|
||||||
<el-button size="small" :icon="NextIcon" :disabled="page >= max" @click="next" aria-label="下一页"></el-button>
|
|
||||||
<el-button size="small" :icon="LastIcon" :disabled="page >= max" @click="toMax" aria-label="最后一页"></el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<OnlineReader/>
|
|
||||||
|
|
||||||
<span v-show="!loadComplete" class="loading-text">请输入授权码后再查看</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import store from "../store";
|
|
||||||
import {computed, h, ref, watch} from "vue";
|
|
||||||
import OnlineReader from "./OnlineReader.vue";
|
|
||||||
import AppIcon from "./AppIcon.vue";
|
|
||||||
|
|
||||||
// el-button takes a component through :icon, so wrap the shared icon component.
|
|
||||||
const icon = (name) => ({ render: () => h(AppIcon, { name }) });
|
|
||||||
|
|
||||||
const DownloadIcon = icon('download');
|
|
||||||
const EyeIcon = icon('eye');
|
|
||||||
const StarIcon = icon('star');
|
|
||||||
const RefreshIcon = icon('refresh');
|
|
||||||
const TrashIcon = icon('trash');
|
|
||||||
const FirstIcon = icon('chevrons-left');
|
|
||||||
const PrevIcon = icon('chevron-left');
|
|
||||||
const NextIcon = icon('chevron-right');
|
|
||||||
const LastIcon = icon('chevrons-right');
|
|
||||||
|
|
||||||
//输入
|
|
||||||
let inputNode = ref(null)
|
|
||||||
//是否正在编辑页数
|
|
||||||
let isEditingPage = ref(false)
|
|
||||||
|
|
||||||
let category = computed(() => {
|
|
||||||
return store.state.category
|
|
||||||
})
|
|
||||||
let galleryNameType = computed(() => {
|
|
||||||
return store.state.galleryNameType
|
|
||||||
})
|
|
||||||
let sortType = computed(() => {
|
|
||||||
return store.state.sortType
|
|
||||||
})
|
|
||||||
let targetPage = ref(1) // 当前页数
|
|
||||||
let username = computed(() => {
|
|
||||||
return store.state.username
|
|
||||||
})
|
|
||||||
|
|
||||||
//防抖计时器
|
|
||||||
let debounceTimer = 0
|
|
||||||
let retryingGids = ref(new Set())
|
|
||||||
//是否加载完成
|
|
||||||
let loadComplete = computed(() => {
|
|
||||||
return store.state.loadComplete
|
|
||||||
})
|
|
||||||
|
|
||||||
let currentTasks = computed(() => {
|
|
||||||
return store.getters.currentTasks ? store.getters.currentTasks: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const connectionText = computed(() => ({
|
|
||||||
connected: '进度已连接',
|
|
||||||
connecting: '正在连接进度',
|
|
||||||
reconnecting: '连接中断,正在重连',
|
|
||||||
disconnected: '进度连接已断开',
|
|
||||||
})[store.state.connectionStatus])
|
|
||||||
|
|
||||||
const connectionTagType = computed(() =>
|
|
||||||
store.state.connectionStatus === 'connected' ? 'success' : 'warning'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reserve room for the header and the toolbar so a long list scrolls internally
|
|
||||||
// instead of pushing the pager out of the viewport.
|
|
||||||
const tableMaxHeight = computed(() => 'calc(100vh - 170px)')
|
|
||||||
|
|
||||||
// States come from the backend as Chinese labels; map them to pill colours.
|
|
||||||
function statusClass(status) {
|
|
||||||
if (status === '下载完成') return 'is-done'
|
|
||||||
if (status === '下载中' || status === '压缩中') return 'is-running'
|
|
||||||
return 'is-waiting'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only in-flight downloads have a meaningful numeric percentage.
|
|
||||||
function progressPercent(task) {
|
|
||||||
if (task.status !== '下载中' || !task.pages) return null
|
|
||||||
const done = Number(task.proceeding) || 0
|
|
||||||
return Math.min(100, Math.max(0, Math.round((done / task.pages) * 100)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function progressLabel(task) {
|
|
||||||
const percent = progressPercent(task)
|
|
||||||
if (percent === null) return task.progress ?? ''
|
|
||||||
return `${percent}% · ${task.proceeding ?? 0}/${task.pages} 页`
|
|
||||||
}
|
|
||||||
|
|
||||||
let min = computed(() => {
|
|
||||||
return store.getters.min
|
|
||||||
})
|
|
||||||
let max = computed(() => {
|
|
||||||
return store.getters.max
|
|
||||||
})
|
|
||||||
let page = computed(() => {
|
|
||||||
return store.state.page
|
|
||||||
})
|
|
||||||
let isLion = computed(() => {
|
|
||||||
return store.state.userId === 3
|
|
||||||
})
|
|
||||||
|
|
||||||
// 同步 store.page → local targetPage,并在 page 超出 max 时修正
|
|
||||||
watch(page, (newPage) => {
|
|
||||||
targetPage.value = newPage
|
|
||||||
})
|
|
||||||
watch(max, (newMax) => {
|
|
||||||
if(targetPage.value > newMax)
|
|
||||||
store.commit("_changePage", newMax)
|
|
||||||
})
|
|
||||||
|
|
||||||
let emptyText = computed(() => {
|
|
||||||
let action = category.value === 'myDownload' ? '下载': '收藏'
|
|
||||||
return '您未' + action + "过"
|
|
||||||
})
|
|
||||||
|
|
||||||
//翻页
|
|
||||||
function next() {
|
|
||||||
if(targetPage.value < max.value) {
|
|
||||||
targetPage.value++
|
|
||||||
store.commit("_changePage", targetPage.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function previous() {
|
|
||||||
if(targetPage.value > min.value) {
|
|
||||||
targetPage.value--
|
|
||||||
store.commit("_changePage", targetPage.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function toMax() {
|
|
||||||
store.commit("_changePage", max.value)
|
|
||||||
targetPage.value = max.value
|
|
||||||
}
|
|
||||||
function toMin(){
|
|
||||||
store.commit("_changePage", min.value)
|
|
||||||
targetPage.value = min.value
|
|
||||||
}
|
|
||||||
function changePage(){
|
|
||||||
if(targetPage.value >= min.value && targetPage.value <= max.value)
|
|
||||||
store.commit("_changePage", targetPage.value)
|
|
||||||
}
|
|
||||||
function reverseEditMode(){
|
|
||||||
isEditingPage.value = !isEditingPage.value
|
|
||||||
if(isEditingPage.value){
|
|
||||||
inputNode.value.focus()
|
|
||||||
}
|
|
||||||
targetPage.value = page.value
|
|
||||||
}
|
|
||||||
|
|
||||||
//改变展示类型
|
|
||||||
function changeCategory(value){
|
|
||||||
store.commit("_setCategory", value)
|
|
||||||
}
|
|
||||||
function changeGalleryNameType(value){
|
|
||||||
store.commit("_setGalleryNameType", value)
|
|
||||||
}
|
|
||||||
function changeSortType(value){
|
|
||||||
store.commit("_setSortType", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
//收藏
|
|
||||||
function changeGalleryCollect(gid, isCollect){
|
|
||||||
if(isCollect)
|
|
||||||
store.dispatch("disCollectGallery", gid)
|
|
||||||
else
|
|
||||||
store.dispatch("collectGallery", gid)
|
|
||||||
}
|
|
||||||
|
|
||||||
//下载,删除,在线看
|
|
||||||
function downloadTask(link){
|
|
||||||
window.open(link)
|
|
||||||
}
|
|
||||||
function deleteGallery(gid){
|
|
||||||
store.dispatch("deleteGallery", gid)
|
|
||||||
}
|
|
||||||
async function retryGallery(gid){
|
|
||||||
if(retryingGids.value.has(gid))
|
|
||||||
return
|
|
||||||
retryingGids.value.add(gid)
|
|
||||||
try {
|
|
||||||
await store.dispatch("retryGallery", gid)
|
|
||||||
} finally {
|
|
||||||
retryingGids.value.delete(gid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function readOnlineGallery(gallery){
|
|
||||||
store.dispatch("readOnlineGallery", gallery)
|
|
||||||
}
|
|
||||||
|
|
||||||
//显示缩略图
|
|
||||||
function showThumbnail(gallery){
|
|
||||||
if(gallery.status === "下载完成" && 'thumb_link' in gallery) {
|
|
||||||
clearTimeout(debounceTimer)
|
|
||||||
debounceTimer = setTimeout(() => {
|
|
||||||
store.commit("_changeThumbnailGallery", gallery)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.side-table-wrapper {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.load_complete {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 16px 16px 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 200px;
|
|
||||||
color: var(--text-secondary, #86909c);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
display: inline-block;
|
|
||||||
width: 46px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page.el-input {
|
|
||||||
width: 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page:hover {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import AppIcon from "./AppIcon.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
gallery: {type: Object, required: true},
|
||||||
|
nameType: {type: String, default: "shortName"},
|
||||||
|
selected: {type: Boolean, default: false},
|
||||||
|
retrying: {type: Boolean, default: false},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["open", "download", "retry", "collect"]);
|
||||||
|
|
||||||
|
const title = computed(() =>
|
||||||
|
props.nameType === "shortName" ? props.gallery.shortName : props.gallery.name);
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = props.gallery.status;
|
||||||
|
if (status === "下载完成") return "is-done";
|
||||||
|
if (status === "下载中" || status === "压缩中") return "is-running";
|
||||||
|
return "is-waiting";
|
||||||
|
});
|
||||||
|
|
||||||
|
const percent = computed(() => {
|
||||||
|
const task = props.gallery;
|
||||||
|
if (task.status !== "下载中" || !task.pages) return null;
|
||||||
|
const done = Number(task.proceeding) || 0;
|
||||||
|
return Math.min(100, Math.max(0, Math.round((done / task.pages) * 100)));
|
||||||
|
});
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
emit("open", props.gallery);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="task-row" :class="{'is-selected': selected}" @click="open">
|
||||||
|
<span class="task-thumb">
|
||||||
|
<img v-if="gallery.thumb_link" :src="gallery.thumb_link" alt="" loading="lazy">
|
||||||
|
<AppIcon v-else name="image" :size="16" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="task-body">
|
||||||
|
<p class="task-title">{{ title }}</p>
|
||||||
|
<p class="task-meta">
|
||||||
|
<span>{{ gallery.pages }} 页</span>
|
||||||
|
<span>{{ gallery.language }}</span>
|
||||||
|
<span>{{ gallery.fileSize }}</span>
|
||||||
|
</p>
|
||||||
|
<div v-if="percent !== null" class="task-progress">
|
||||||
|
<el-progress :percentage="percent" :stroke-width="4" :show-text="false" />
|
||||||
|
<span class="progress-value">{{ percent }}% · {{ gallery.proceeding }}/{{ gallery.pages }} 页</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-side">
|
||||||
|
<span class="status-pill" :class="statusClass">{{ gallery.status }}</span>
|
||||||
|
<div class="task-acts" @click.stop>
|
||||||
|
<button type="button"
|
||||||
|
class="icon-button"
|
||||||
|
title="下载文件"
|
||||||
|
aria-label="下载文件"
|
||||||
|
:disabled="gallery.status !== '下载完成'"
|
||||||
|
@click="emit('download', gallery)">
|
||||||
|
<AppIcon name="download" :size="15" />
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="icon-button"
|
||||||
|
:class="{'is-on': gallery.isCollect}"
|
||||||
|
:title="gallery.isCollect ? '取消收藏' : '收藏'"
|
||||||
|
:aria-label="gallery.isCollect ? '取消收藏' : '收藏'"
|
||||||
|
@click="emit('collect', gallery)">
|
||||||
|
<AppIcon name="star" :size="15" />
|
||||||
|
</button>
|
||||||
|
<button v-if="gallery.status !== '下载完成'"
|
||||||
|
type="button"
|
||||||
|
class="icon-button"
|
||||||
|
title="重试任务"
|
||||||
|
aria-label="重试任务"
|
||||||
|
:disabled="retrying"
|
||||||
|
@click="emit('retry', gallery)">
|
||||||
|
<AppIcon name="refresh" :size="15" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import TaskRow from "./TaskRow.vue";
|
||||||
|
import AppIcon from "./AppIcon.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedGid: {type: [Number, String], default: null},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["open"]);
|
||||||
|
|
||||||
|
const retryingGids = ref(new Set());
|
||||||
|
const refreshing = ref(false);
|
||||||
|
const isEditingPage = ref(false);
|
||||||
|
const inputNode = ref(null);
|
||||||
|
|
||||||
|
const currentTasks = computed(() => store.getters.currentTasks || []);
|
||||||
|
const total = computed(() => (store.getters.filteredTasks || []).length);
|
||||||
|
const isSearching = computed(() => store.state.isSearch);
|
||||||
|
|
||||||
|
const category = computed({
|
||||||
|
get: () => store.state.category,
|
||||||
|
set: value => store.commit("_setCategory", value),
|
||||||
|
});
|
||||||
|
const sortType = computed({
|
||||||
|
get: () => store.state.sortType,
|
||||||
|
set: value => store.commit("_setSortType", value),
|
||||||
|
});
|
||||||
|
const galleryNameType = computed({
|
||||||
|
get: () => store.state.galleryNameType,
|
||||||
|
set: value => store.commit("_setGalleryNameType", value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAdmin = computed(() => store.state.isAdmin);
|
||||||
|
const downloaderFilter = computed({
|
||||||
|
get: () => store.state.downloaderFilter,
|
||||||
|
set: value => store.commit("_setDownloaderFilter", value),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Options come from the current category, so the dropdown can never offer a
|
||||||
|
// downloader that would filter the visible list down to nothing.
|
||||||
|
const downloaderOptions = computed(() => {
|
||||||
|
const seen = new Map();
|
||||||
|
;(store.state.currentTasks || []).forEach(task => {
|
||||||
|
if (task.downloaderName && !seen.has(task.downloader))
|
||||||
|
seen.set(task.downloader, task.downloaderName);
|
||||||
|
});
|
||||||
|
return [...seen].map(([id, name]) => ({id, name}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const isConnected = computed(() => store.state.connectionStatus === "connected");
|
||||||
|
const connectionLabel = computed(() => ({
|
||||||
|
connected: "进度已连接",
|
||||||
|
connecting: "正在连接进度",
|
||||||
|
reconnecting: "连接中断,正在重连",
|
||||||
|
disconnected: "进度连接已断开",
|
||||||
|
})[store.state.connectionStatus]);
|
||||||
|
|
||||||
|
const emptyText = computed(() => {
|
||||||
|
if (isSearching.value) return "没有匹配的任务";
|
||||||
|
if (category.value === "myDownload") return "您未下载过";
|
||||||
|
if (category.value === "myCollect") return "您未收藏过";
|
||||||
|
return "暂无任务";
|
||||||
|
});
|
||||||
|
|
||||||
|
const min = computed(() => store.getters.min);
|
||||||
|
const max = computed(() => store.getters.max);
|
||||||
|
const page = computed(() => store.state.page);
|
||||||
|
const targetPage = ref(1);
|
||||||
|
|
||||||
|
watch(page, value => {
|
||||||
|
targetPage.value = value;
|
||||||
|
}, {immediate: true});
|
||||||
|
|
||||||
|
function goto(value) {
|
||||||
|
if (value < min.value || value > max.value) return;
|
||||||
|
store.commit("_changePage", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reverseEditMode() {
|
||||||
|
isEditingPage.value = !isEditingPage.value;
|
||||||
|
if (isEditingPage.value) inputNode.value?.focus();
|
||||||
|
targetPage.value = page.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTask(gallery) {
|
||||||
|
if (gallery.download) window.open(gallery.download);
|
||||||
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCollect(gallery) {
|
||||||
|
if (gallery.isCollect) store.dispatch("disCollectGallery", gallery.gid);
|
||||||
|
else store.dispatch("collectGallery", gallery.gid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryGallery(gallery) {
|
||||||
|
if (retryingGids.value.has(gallery.gid)) return;
|
||||||
|
retryingGids.value.add(gallery.gid);
|
||||||
|
try {
|
||||||
|
await store.dispatch("retryGallery", gallery.gid);
|
||||||
|
} finally {
|
||||||
|
retryingGids.value.delete(gallery.gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTasks() {
|
||||||
|
if (refreshing.value) return;
|
||||||
|
refreshing.value = true;
|
||||||
|
try {
|
||||||
|
await store.dispatch("updateGalleryTasks");
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
store.commit("_searchLocalByKeyword", "");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view">
|
||||||
|
<header class="view-bar">
|
||||||
|
<h1 class="view-title">任务</h1>
|
||||||
|
<span class="view-count">{{ total }} 项</span>
|
||||||
|
<span class="connection" :class="{'is-connected': isConnected}" role="status" :title="connectionLabel">
|
||||||
|
<i class="connection-dot"></i>{{ connectionLabel }}
|
||||||
|
</span>
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
<button type="button"
|
||||||
|
class="text-button"
|
||||||
|
:disabled="refreshing"
|
||||||
|
@click="refreshTasks">
|
||||||
|
<AppIcon name="refresh" :size="15" />刷新
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<div class="filter">
|
||||||
|
<span class="filter-label">分类</span>
|
||||||
|
<el-select v-model="category" class="filter-select filter-select-category" size="small">
|
||||||
|
<el-option value="myDownload" label="我的下载" />
|
||||||
|
<el-option value="myCollect" label="我的收藏" />
|
||||||
|
<el-option value="total" label="全部" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<span class="filter-label">排序</span>
|
||||||
|
<el-select v-model="sortType" class="filter-select filter-select-sort" size="small">
|
||||||
|
<el-option value="shortName" label="简洁名字" />
|
||||||
|
<el-option value="name" label="名字" />
|
||||||
|
<el-option value="createTime" label="任务创建时间" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<span class="filter-label">显示</span>
|
||||||
|
<el-select v-model="galleryNameType" class="filter-select filter-select-name" size="small">
|
||||||
|
<el-option value="shortName" label="简洁名字" />
|
||||||
|
<el-option value="name" label="名字" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div v-if="isAdmin" class="filter">
|
||||||
|
<span class="filter-label">下载人</span>
|
||||||
|
<el-select v-model="downloaderFilter"
|
||||||
|
class="filter-select filter-select-downloader"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
placeholder="全部">
|
||||||
|
<el-option v-for="item in downloaderOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
:label="item.name" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="isSearching" type="button" class="chip-button" @click="clearSearch">
|
||||||
|
清除搜索
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="view-bar-spacer"></span>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<button type="button" class="icon-button" title="第一页" :disabled="page <= min" @click="goto(min)">
|
||||||
|
<AppIcon name="chevrons-left" :size="15" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="icon-button" title="上一页" :disabled="page <= min" @click="goto(page - 1)">
|
||||||
|
<AppIcon name="chevron-left" :size="15" />
|
||||||
|
</button>
|
||||||
|
<span class="pager-readout">
|
||||||
|
<el-input v-if="isEditingPage"
|
||||||
|
v-model="targetPage"
|
||||||
|
class="page-input"
|
||||||
|
size="small"
|
||||||
|
ref="inputNode"
|
||||||
|
@change="goto(Number(targetPage))"
|
||||||
|
@blur="reverseEditMode" />
|
||||||
|
<button v-else type="button" class="page-current" @click="reverseEditMode">{{ page }}</button>
|
||||||
|
<span class="page-total">/ {{ max }}</span>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="icon-button" title="下一页" :disabled="page >= max" @click="goto(page + 1)">
|
||||||
|
<AppIcon name="chevron-right" :size="15" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="icon-button" title="最后一页" :disabled="page >= max" @click="goto(max)">
|
||||||
|
<AppIcon name="chevrons-right" :size="15" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-scroll">
|
||||||
|
<p v-if="currentTasks.length === 0" class="empty-state">{{ emptyText }}</p>
|
||||||
|
<TaskRow v-for="gallery in currentTasks"
|
||||||
|
:key="gallery.gid"
|
||||||
|
:gallery="gallery"
|
||||||
|
:name-type="galleryNameType"
|
||||||
|
:selected="String(gallery.gid) === String(selectedGid)"
|
||||||
|
:retrying="retryingGids.has(gallery.gid)"
|
||||||
|
@open="emit('open', $event)"
|
||||||
|
@download="downloadTask"
|
||||||
|
@collect="toggleCollect"
|
||||||
|
@retry="retryGallery" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+42
-13
@@ -154,7 +154,8 @@ const actions = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteGallery(context, gid){
|
deleteGallery(context, gid){
|
||||||
axios.delete(GalleryManageUrl, {
|
// Returned so callers can clear a selection once the task is really gone.
|
||||||
|
return axios.delete(GalleryManageUrl, {
|
||||||
params:{
|
params:{
|
||||||
AuthCode:context.state.AuthCode, gid
|
AuthCode:context.state.AuthCode, gid
|
||||||
}}).then((res) => {
|
}}).then((res) => {
|
||||||
@@ -315,6 +316,8 @@ const mutations = {
|
|||||||
state.AuthCode = data.AuthCode
|
state.AuthCode = data.AuthCode
|
||||||
state.userId = data.userId
|
state.userId = data.userId
|
||||||
state.username = data.username
|
state.username = data.username
|
||||||
|
state.isAdmin = Boolean(data.isAdmin)
|
||||||
|
state.downloaderFilter = null
|
||||||
state.isAuth = true
|
state.isAuth = true
|
||||||
ElMessage("验证成功,加载中")
|
ElMessage("验证成功,加载中")
|
||||||
},
|
},
|
||||||
@@ -381,6 +384,8 @@ const mutations = {
|
|||||||
},
|
},
|
||||||
_setCategory(state, category){
|
_setCategory(state, category){
|
||||||
state.category = category
|
state.category = category
|
||||||
|
// 换分类后旧的下载人筛选可能在新分类里根本不存在,先清掉。
|
||||||
|
state.downloaderFilter = null
|
||||||
confirmCurrentTask(state)
|
confirmCurrentTask(state)
|
||||||
sortTasks(state)
|
sortTasks(state)
|
||||||
},
|
},
|
||||||
@@ -412,8 +417,18 @@ const mutations = {
|
|||||||
_closeReader(state){
|
_closeReader(state){
|
||||||
state.isReading = false;
|
state.isReading = false;
|
||||||
},
|
},
|
||||||
_changeThumbnailGallery(state, gallery){
|
_setActiveView(state, view){
|
||||||
state.thumbnailGallery = gallery
|
state.activeView = view
|
||||||
|
},
|
||||||
|
_selectGallery(state, gallery){
|
||||||
|
state.selectedGallery = gallery || null
|
||||||
|
},
|
||||||
|
_setDownloaderFilter(state, downloader){
|
||||||
|
state.downloaderFilter = downloader === null || downloader === undefined
|
||||||
|
? null
|
||||||
|
: Number(downloader)
|
||||||
|
// 筛完可能不足一页,回到第一页避免停在空页上。
|
||||||
|
state.page = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +437,6 @@ const state = {
|
|||||||
|
|
||||||
totalGalleryTask: [], //存放数据的数组
|
totalGalleryTask: [], //存放数据的数组
|
||||||
chosenGallery: false, //准备下载
|
chosenGallery: false, //准备下载
|
||||||
thumbnailGallery: {}, //缩略图
|
|
||||||
collectGallery: [], //收藏
|
collectGallery: [], //收藏
|
||||||
downloadGallery: [], //下载
|
downloadGallery: [], //下载
|
||||||
isSearch: false, //用于决定是否显示搜索结果
|
isSearch: false, //用于决定是否显示搜索结果
|
||||||
@@ -452,25 +466,28 @@ const state = {
|
|||||||
galleryNameType: 'shortName', //名字类型 shortName name
|
galleryNameType: 'shortName', //名字类型 shortName name
|
||||||
currentTasks: [], //当前任务
|
currentTasks: [], //当前任务
|
||||||
weekUsed: {}, //每周用量
|
weekUsed: {}, //每周用量
|
||||||
|
|
||||||
|
activeView: 'tasks', //当前视图 tasks search settings
|
||||||
|
selectedGallery: null, //右侧详情面板选中的任务
|
||||||
|
isAdmin: false, //管理员(后端 validate 下发)
|
||||||
|
downloaderFilter: null, //管理员按下载人筛选,null 表示全部
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
|
/** 当前筛选条件命中的完整任务列表(未分页),用于计数。 */
|
||||||
|
filteredTasks(state){
|
||||||
|
return activeTaskList(state)
|
||||||
|
},
|
||||||
currentTasks(state){
|
currentTasks(state){
|
||||||
if(state.isSearch)
|
const tasks = activeTaskList(state)
|
||||||
return state.searchTask.slice((state.page - 1) * state.length, state.page * state.length)
|
return tasks.slice((state.page - 1) * state.length, state.page * state.length)
|
||||||
else
|
|
||||||
return state.currentTasks.slice((state.page - 1) * state.length, state.page * state.length)
|
|
||||||
},
|
},
|
||||||
min(){
|
min(){
|
||||||
return 1
|
return 1
|
||||||
},
|
},
|
||||||
max(state){
|
max(state){
|
||||||
let max = 0
|
let max = 0
|
||||||
let tasks
|
const tasks = activeTaskList(state)
|
||||||
if(state.isSearch)
|
|
||||||
tasks = state.searchTask
|
|
||||||
else
|
|
||||||
tasks = state.currentTasks
|
|
||||||
|
|
||||||
if(!tasks)
|
if(!tasks)
|
||||||
return 1
|
return 1
|
||||||
@@ -490,6 +507,18 @@ export default new vuex.Store({
|
|||||||
getters
|
getters
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前视图实际要展示的任务。
|
||||||
|
* 下载人筛选只在管理员生效,作用于「我的下载/收藏/全部」三种分类的结果。
|
||||||
|
*/
|
||||||
|
function activeTaskList(state){
|
||||||
|
if(state.isSearch)
|
||||||
|
return state.searchTask
|
||||||
|
if(state.isAdmin && state.downloaderFilter !== null)
|
||||||
|
return state.currentTasks.filter(task => task.downloader === state.downloaderFilter)
|
||||||
|
return state.currentTasks
|
||||||
|
}
|
||||||
|
|
||||||
function getShortname(name){
|
function getShortname(name){
|
||||||
if(name === null){
|
if(name === null){
|
||||||
console.log(name)
|
console.log(name)
|
||||||
|
|||||||
+748
-280
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
const BaseUrl = "https://downloader.lionwebsite.xyz/";
|
||||||
|
|
||||||
|
// Remote galleries carry a raw thumbnail path; it only resolves through the
|
||||||
|
// backend proxy once the active auth code is attached.
|
||||||
|
export function ehThumbnailUrl(path, AuthCode) {
|
||||||
|
if (!path) return "";
|
||||||
|
return BaseUrl + "GalleryManage/ehThumbnail?" + new URLSearchParams({path, AuthCode}).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const UNIT_SCALE = {b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4};
|
||||||
|
|
||||||
|
function sizeInBytes(text) {
|
||||||
|
const match = String(text).match(/([\d.]+)\s*([kmgt]?)(?:i?b)/i);
|
||||||
|
if (!match) return 0;
|
||||||
|
const scale = UNIT_SCALE[match[2].toLowerCase()] ?? 1;
|
||||||
|
return Number(match[1]) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the original file; otherwise take whichever entry reports the most bytes.
|
||||||
|
export function pickDefaultResolution(availableResolution) {
|
||||||
|
const entries = Object.entries(availableResolution || {});
|
||||||
|
if (entries.length === 0) return "";
|
||||||
|
const original = entries.find(([resolution]) => /original/i.test(resolution));
|
||||||
|
if (original) return original[0];
|
||||||
|
return entries.reduce((best, current) =>
|
||||||
|
sizeInBytes(current[1]) > sizeInBytes(best[1]) ? current : best)[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import {pickDefaultResolution, ehThumbnailUrl} from '../src/utils/thumbnail.js'
|
||||||
|
|
||||||
|
test('original resolution wins whenever it is offered', () => {
|
||||||
|
assert.equal(pickDefaultResolution({
|
||||||
|
'800x': '37.53MiB',
|
||||||
|
'1280x': '66.57MiB',
|
||||||
|
'2560x': '84.01MiB',
|
||||||
|
'Original': '498.2MiB'
|
||||||
|
}), 'Original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('without an original entry the largest file is chosen', () => {
|
||||||
|
// 1920x is bigger than 2560x here, so byte size decides, not the label.
|
||||||
|
assert.equal(pickDefaultResolution({
|
||||||
|
'800x': '37.53MiB',
|
||||||
|
'1920x': '811.1MiB',
|
||||||
|
'2560x': '84.01MiB'
|
||||||
|
}), '1920x')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('an empty or missing resolution map yields an empty choice', () => {
|
||||||
|
assert.equal(pickDefaultResolution({}), '')
|
||||||
|
assert.equal(pickDefaultResolution(undefined), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('thumbnail urls carry the path and the active auth code', () => {
|
||||||
|
const url = new URL(ehThumbnailUrl('https://example.org/t?a=1&b=2', 'code with spaces'))
|
||||||
|
assert.equal(url.searchParams.get('path'), 'https://example.org/t?a=1&b=2')
|
||||||
|
assert.equal(url.searchParams.get('AuthCode'), 'code with spaces')
|
||||||
|
assert.equal(ehThumbnailUrl('', 'x'), '')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user