桌面下载端改用共享核心包,并修两处已知缺陷

- store 改由 createGalleryStore 生成:totalGalleryTask 成为唯一任务源,
  收藏/下载/分类/搜索结果全部由 getter 派生,不再维护三份互相引用的数组
- 排序不再原地修改分类数组;本地搜索记住关键词,任务刷新后结果仍在
- 本地搜索结果补齐翻页入口:此前匹配 25 项却只显示 10 项,其余无法看到
- 取消收藏不再因本地找不到 gid 抛 TypeError
- 删除重复的 taskSocket/progressiveImages/validate/thumbnail,改从共享包导入
- 远端搜索改用共享 fetchGalleries,关键词交给 axios 序列化,翻页按游标转发
- main.js 按需注册 Element Plus,入口 JS 由约 1.14MB 降到约 494KB
- 依赖新增 file:../lionwebsite-frontend-shared

测试:14 项通过;构建成功,入口仍为 /asserts/index/index.js|css。
This commit is contained in:
root
2026-10-07 16:15:23 +08:00
parent 5208b7ccec
commit 97d635fbc7
17 changed files with 208 additions and 953 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ 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";
import {ehThumbnailUrl, pickDefaultResolution} from "lionwebsite-frontend-shared";
const targetResolution = ref("");
const isRetrying = ref(false);
+2 -2
View File
@@ -1,9 +1,9 @@
<script setup>
import {computed, nextTick, onBeforeUnmount, ref, watch} from "vue";
import {startImages, settleImage, retryImage} from "../utils/progressiveImages.js";
import {startImages, settleImage, retryImage} from "lionwebsite-frontend-shared";
import store from "../store/index.js";
import AppIcon from "./AppIcon.vue";
import {ehThumbnailUrl} from "../utils/thumbnail.js";
import {ehThumbnailUrl} from "lionwebsite-frontend-shared";
const viewer = ref(null);
const readingGallery = computed(() => store.state.readingGallery);
+30 -29
View File
@@ -5,8 +5,7 @@ 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";
import {ehThumbnailUrl, fetchGalleries, validateLink} from "lionwebsite-frontend-shared";
const mode = ref("ehentai");
const localMode = ref("keyword");
@@ -20,8 +19,16 @@ const remoteLoading = ref(false);
const retryingGids = ref(new Set());
const localResults = computed(() => store.getters.currentTasks || []);
const localTotal = computed(() => store.state.searchTask.length);
const localTotal = computed(() => (store.getters.filteredTasks || []).length);
const isSearching = computed(() => store.state.isSearch);
const localPage = computed(() => store.state.page);
const localMin = computed(() => store.getters.min);
const localMax = computed(() => store.getters.max);
function gotoLocalPage(value) {
if (value < localMin.value || value > localMax.value) return;
store.commit("_changePage", value);
}
function runLocalSearch() {
if (localKeyword.value.trim() === "") {
@@ -37,32 +44,20 @@ function clearLocalSearch() {
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;
}
// direction 为 "next"/"prev" 时使用后端返回的翻页游标;href 为空表示新关键词。
function queryGalleries(href, direction = null) {
if (href == null && remoteKeyword.value.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"});
}
fetchGalleries(axios, {keyword: remoteKeyword.value, direction, href: href || ""})
.then(({galleries, pagination}) => {
remotePage.value = pagination;
remoteGalleries.value.splice(0, remoteGalleries.value.length, ...galleries);
})
.catch(error => {
ElMessage({message: error.payload || error.message || "搜索失败", type: "error"});
})
.finally(() => {
remoteLoading.value = false;
@@ -159,8 +154,8 @@ async function retryGallery(gallery) {
<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.previous === undefined" @click="queryGalleries(remotePage.previous, 'prev')">上一页</el-button>
<el-button size="small" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.next, 'next')">下一页</el-button>
<el-button size="small" :disabled="remotePage.last === undefined" @click="queryGalleries(remotePage.last)">尾页</el-button>
</div>
</template>
@@ -190,6 +185,12 @@ async function retryGallery(gallery) {
@open="store.commit('_selectGallery', $event)"
@download="downloadTask"
@retry="retryGallery" />
<!-- 命中项可能多于每页条数,必须给出翻页入口,否则后面的结果看不到。 -->
<div class="pager-row" v-if="localMax > 1">
<el-button size="small" :disabled="localPage <= localMin" @click="gotoLocalPage(localPage - 1)">上一页</el-button>
<span class="hint">第 {{ localPage }} / {{ localMax }} 页</span>
<el-button size="small" :disabled="localPage >= localMax" @click="gotoLocalPage(localPage + 1)">下一页</el-button>
</div>
</template>
<p v-else class="empty-state">输入关键字或链接在已有任务里查找</p>
</template>
+1 -1
View File
@@ -42,7 +42,7 @@ const downloaderFilter = computed({
// downloader that would filter the visible list down to nothing.
const downloaderOptions = computed(() => {
const seen = new Map();
;(store.state.currentTasks || []).forEach(task => {
;(store.getters.categoryGallery || []).forEach(task => {
if (task.downloaderName && !seen.has(task.downloader))
seen.set(task.downloader, task.downloaderName);
});
+11 -2
View File
@@ -1,9 +1,18 @@
import { createApp } from 'vue'
import App from './App.vue'
import {
ElButton, ElCheckbox, ElDialog, ElForm, ElFormItem, ElImage, ElInput,
ElInputNumber, ElOption, ElProgress, ElScrollbar, ElSelect, ElSwitch,
} from "element-plus"
import 'element-plus/dist/index.css'
import element from "element-plus"
import "./reset.css"
import 'element-plus/theme-chalk/dark/css-vars.css'
import "./styles/theme.css"
createApp(App).use(element).mount('#app')
// 只注册本仓库用到的组件,避免把整个 Element Plus 打进入口包。
// ElMessage 是函数式 API,按需在组件里 import 即可。
const app = createApp(App)
for (const component of [ElButton, ElCheckbox, ElDialog, ElForm, ElFormItem, ElImage,
ElInput, ElInputNumber, ElOption, ElProgress, ElScrollbar, ElSelect, ElSwitch])
app.component(component.name, component)
app.mount('#app')
+8 -576
View File
@@ -2,11 +2,7 @@ import vuex from "vuex"
import axios from "axios"
import {ElMessage} from "element-plus"
import qs from "qs"
import {createTaskSocket} from "../utils/taskSocket.js"
let taskSocket
let taskRefreshVersion = 0
const BaseUrl = "https://downloader.lionwebsite.xyz/"
const GalleryManageUrl = BaseUrl + "GalleryManage"
import {createGalleryStore} from "lionwebsite-frontend-shared"
// Axios 全局错误处理
axios.interceptors.response.use(
@@ -17,574 +13,10 @@ axios.interceptors.response.use(
}
)
const actions = {
reconnect(context) {
axios.post(GalleryManageUrl + "/reconnect", null, {
params: {
AuthCode: context.state.AuthCode
}
}).then(res => {
if (res.data.result === "success") {
ElMessage("重连成功")
} else {
ElMessage("重连失败")
}
})
},
updateGalleryTasks(context){
const version = ++taskRefreshVersion
return axios.get(GalleryManageUrl, {
params:{
AuthCode: context.state.AuthCode,
type: 'all'
}
}).then((res) => {
if(res.data.result === "success" && version === taskRefreshVersion)
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
})
},
postGalleryTask(context, data){
return axios.post(GalleryManageUrl + '?' + qs.stringify({
AuthCode: context.state.AuthCode,
...data
}, {indices:false})).then((res) => {
if(res.data.result === "success") {
ElMessage("提交成功")
context.commit("_setChosenGallery", {gallery: false})
}
else
if(res.data.data)
ElMessage(res.data.data)
else
ElMessage("提交失败")
// A node timeout can still leave a persisted task; always reload its actual state.
return Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")])
})
},
queryGalleryTask(context, link){
axios.get(GalleryManageUrl, {
params:{
param: link,
type: 'link',
AuthCode: context.state.AuthCode
}
}).then((res) => {
if(res.data.result === 'success'){
let gallery = JSON.parse(res.data.data)
if(gallery.status === "下载完成")
gallery.download = buildGalleryDownloadUrl(gallery.gid, context.state.AuthCode)
context.commit("_setChosenGallery", {gallery})
}
else
ElMessage("查询失败")
})
},
validate(context, AuthCode){
axios.post(BaseUrl + "validate?AuthCode=" + AuthCode).then((res)=>{
if(res.data.result === 'success'){
let data = JSON.parse(res.data.data);
if(!data.isAvailable)
ElMessage({duration:0, message:"节点挂了,不能下也不能看,找狮子处理", type: "error"})
context.commit("_authed", {AuthCode, ...data})
//初始化
context.dispatch("loadWeekUsedAmount").then()
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
context.dispatch("initWebsocket").then()
}
else {
context.dispatch("disconnectWebsocket")
context.commit("_unAuthed")
}
})
},
initWebsocket(context){
taskSocket?.stop()
taskSocket = createTaskSocket({
url: "wss://downloader.lionwebsite.xyz/ws/",
onState: status => context.commit("_setConnectionStatus", status),
onOpen: () => {
// Catch up on every connection, including events missed during an outage.
Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")]).catch(() => {})
},
onMessage: event => {
let message
try { message = JSON.parse(event.data) }
catch { return }
if(message.type === "updateTasks" && Array.isArray(message.data))
context.commit("_updateGalleryTaskProceeding", message.data)
else if(message.type === "fullUpdate")
context.dispatch("updateGalleryTasks").catch(() => {})
}
})
taskSocket.start()
},
disconnectWebsocket(context){
taskSocket?.stop()
taskSocket = undefined
context.commit("_setConnectionStatus", "disconnected")
},
loadWeekUsedAmount(context){
return axios.get(GalleryManageUrl + "/weekUsedAmount", {
params: {
AuthCode: context.state.AuthCode
}
}).then((res) => {
if(res.data.result === "success"){
context.state.weekUsed = JSON.parse(res.data.data)
}else
ElMessage("查询用量失败")
})
},
collectGallery(context, gid){
axios.post(GalleryManageUrl + "/collect?" +qs.stringify( {
gid, AuthCode:context.state.AuthCode
})).then((res) => {
ElMessage(res.data.data)
if(res.data.result === 'success')
context.commit("_collectGallery", gid)
})
},
disCollectGallery(context, gid){
axios.post(GalleryManageUrl + "/disCollect?" + qs.stringify({
gid, AuthCode:context.state.AuthCode
})).then((res) => {
ElMessage(res.data.data)
if(res.data.result === 'success')
context.commit("_disCollectGallery", gid)
})
},
deleteGallery(context, gid){
// Returned so callers can clear a selection once the task is really gone.
return axios.delete(GalleryManageUrl, {
params:{
AuthCode:context.state.AuthCode, gid
}}).then((res) => {
if(res.data.result === "success"){
ElMessage("删除成功")
context.commit("_deleteGallery", gid)
}
else
ElMessage(res.data.data)
})
},
retryGallery(context, gid){
return axios.post(GalleryManageUrl + "/retry?" + qs.stringify({
gid, AuthCode: context.state.AuthCode
})).then((res) => {
if(res.data.result === "success"){
ElMessage({message: "重试结果:" + res.data.data, type: "success"})
return context.dispatch("updateGalleryTasks")
}
ElMessage({message: res.data.data || "重试失败", type: "error"})
})
},
readOnlineGallery(context, gallery){
if(gallery.images !== undefined && gallery.images.length !== 0)
context.commit("_setReadingGallery", gallery)
else
return axios.post(GalleryManageUrl + "/cache", null, {
params: {url: gallery.link, AuthCode: context.state.AuthCode}
}).then((res) => {
if (res.data.result === 'success') {
gallery.pages = res.data.data.pages
setTimeout(() => {
context.commit("_setReadingGallery", gallery)
}, 100)
} else
ElMessage.error(res.data.data)
})
},
alterAuthCode(context, AuthCode){
axios.put(BaseUrl + "AuthCode?" + qs.stringify({'AuthCode': context.state.AuthCode, 'newAuthCode': AuthCode}))
.then((res) => {
if(res.data.result === 'success') {
ElMessage("修改成功")
if(localStorage.getItem("auth") === context.state.AuthCode)
localStorage.setItem("auth", AuthCode)
context.state.AuthCode = AuthCode
}
else
ElMessage(res.data.data)
})
},
resetUndone(context){
axios.post(GalleryManageUrl + "/reset?AuthCode=" + context.state.AuthCode).then((res) => {
ElMessage(res.data.data)
})
}
}
const mutations = {
_setConnectionStatus(state, status){
state.connectionStatus = status
},
_collectGallery(state, gid){
let tasks = state.totalGalleryTask
for(let i=0; i< tasks.length; i++){
if(!tasks[i].isCollect && tasks[i].gid === gid){
tasks[i].isCollect = true
state.collectGallery.push(tasks[i])
}
}
},
_disCollectGallery(state, gid){
let index
for(let i=0; i < state.collectGallery.length; i++)
if(state.collectGallery[i].gid === gid){
index = i
break
}
state.collectGallery[index].isCollect = false
state.collectGallery.splice(index, 1)
},
_updateGalleryTasks(state, tasks){
state.totalGalleryTask.splice(0)
state.collectGallery.splice(0)
state.downloadGallery.splice(0)
tasks.forEach((task) => {
//处理名字
task.shortName = getShortname(task.name)
if(task.thumb_link.trim() !== '')
task.thumb_link = buildGalleryManageUrl("/ehThumbnail", {
path: task.thumb_link,
AuthCode: state.AuthCode
})
else
delete task.thumb_link
//处理进度相关
switch (task.status) {
case "已提交":
case "等待压缩":
case "压缩中":
task.progress = task.status
break;
case "下载中":
task.progress = (Math.round((task.proceeding / task.pages) * 100)).toString() + "%"
break;
case "下载完成":
task.progress = task.status
task.download = buildGalleryDownloadUrl(task.gid, state.AuthCode)
break;
}
//处理时间戳
task.createTimeDisplay = new Date(task.createTime * 1000).toLocaleString("zh")
//处理是否收藏
if('isCollect' in task)
state.collectGallery.push(task)
else
task.isCollect = false
//处理是否下载
if(task.downloader === state.userId)
state.downloadGallery.push(task)
state.totalGalleryTask.push(task)
})
sortTasks(state)
if(state.isAuth && !state.loadComplete){
state.loadComplete = true
ElMessage("加载完成")
}
},
_updateGalleryTaskProceeding(state, tasks){
let galleries = Array.from(state.totalGalleryTask)
state.totalGalleryTask.splice(0)
galleries.forEach((gallery) => {
if(gallery.status !== '下载完成')
tasks.forEach((task) => {
if(task.gid === gallery.gid){
gallery.status = status[task.status]
gallery.proceeding = task.proceeding
if(gallery.status === '下载中')
gallery.progress = (Math.round((gallery.proceeding / gallery.pages) * 100)).toString() + "%"
else
gallery.progress = gallery.status
}
})
state.totalGalleryTask.push(gallery)
})
},
_changePage(state, targetPage){
state.page = targetPage
},
_authed(state, data){
state.AuthCode = data.AuthCode
state.userId = data.userId
state.username = data.username
state.isAdmin = Boolean(data.isAdmin)
state.downloaderFilter = null
state.isAuth = true
ElMessage("验证成功,加载中")
},
_unAuthed(state){
state.isAuth = false
state.AuthCode = ""
state.userId = -1
state.username = ""
ElMessage("授权码错误")
localStorage.removeItem("auth")
},
_searchLocalByLink(state, link){
let tasks = state.currentTasks
let gid = link.split("/")[4]
let name = null
if(gid === undefined)
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].link === link) {
state.page = Math.floor(i / state.length) + 1
name = tasks[i].name
break
}
}
else for (let i = 0; i < tasks.length; i++)
if (String(tasks[i].gid) === gid) {
state.page = Math.floor(i / state.length) + 1
name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name
break
}
if(!name)
ElMessage("未找到此任务")
else
ElMessage("已跳转到该任务所在页数,任务名:" + name)
},
_searchLocalByKeyword(state, keyword){
state.searchTask.splice(0)
if(keyword.trim() !== '') {
state.page = 1
let tasks = state.currentTasks
tasks.forEach((task) => {
if (task.name.includes(keyword))
state.searchTask.push(task)
})
if (state.searchTask.length === 0) {
ElMessage("未找到该关键字的任务")
state.isSearch = false
}
else {
state.isSearch = true
}
}else {
state.isSearch = false
confirmCurrentTask(state)
}
},
_deleteGallery(state, gid){
let tasks = [state.totalGalleryTask, state.downloadGallery, state.collectGallery]
deleteTask(tasks, 'gid', gid)
},
_setChosenGallery(state,data){
state.chosenGallery = data.gallery
},
_setCategory(state, category){
state.category = category
// 换分类后旧的下载人筛选可能在新分类里根本不存在,先清掉。
state.downloaderFilter = null
confirmCurrentTask(state)
sortTasks(state)
},
_setSortType(state, sortType){
state.sortType = sortType
sortTasks(state)
},
_setGalleryNameType(state, galleryNameType){
state.galleryNameType = galleryNameType
},
_setShowNameType(state, type){
if(type === "shortName")
state.length = state.shortLength
else
state.length = state.defaultLength
},
_setReadingGallery(state, gallery){
if(gallery.images === undefined) {
gallery.images = []
for(let i=1; i<=gallery.pages; i++)
gallery.images.push(buildGalleryManageUrl("/onlineImage/" + i, {
gid: gallery.gid,
AuthCode: state.AuthCode
}));
}
state.readingGallery = gallery
state.isReading = true;
},
_closeReader(state){
state.isReading = false;
},
_setActiveView(state, view){
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
}
}
const state = {
connectionStatus: "disconnected", // live task updates
totalGalleryTask: [], //存放数据的数组
chosenGallery: false, //准备下载
collectGallery: [], //收藏
downloadGallery: [], //下载
isSearch: false, //用于决定是否显示搜索结果
readingGallery: {'name': '', 'images': []}, //在线看
isReading: false, //是否正在看
currentGid: "", //当前GID
lengthPerPage: 0, //在线预览每页图片数量
page: 1, //当前页数
length: 10, //每页能有多少个链接
defaultLength: 7, //默认个数
shortLength: 10, //简洁个数
userId: -1, //用户id
username: "", //用户名
isAuth: false, //是否授权
AuthCode: '', //授权码
loadComplete: false, //是否加载完成
galleryRefreshTimer: 0, //更新计时器id
isInclude: false, //是否搜索到任务
searchTask: [], //搜索到的任务
category: 'myDownload', //分类 myDownload myCollect total
sortType:'shortName', //排序类型 shortName name createTime
galleryNameType: 'shortName', //名字类型 shortName name
currentTasks: [], //当前任务
weekUsed: {}, //每周用量
activeView: 'tasks', //当前视图 tasks search settings
selectedGallery: null, //右侧详情面板选中的任务
isAdmin: false, //管理员(后端 validate 下发)
downloaderFilter: null, //管理员按下载人筛选,null 表示全部
}
const getters = {
/** 当前筛选条件命中的完整任务列表(未分页),用于计数。 */
filteredTasks(state){
return activeTaskList(state)
},
currentTasks(state){
const tasks = activeTaskList(state)
return tasks.slice((state.page - 1) * state.length, state.page * state.length)
},
min(){
return 1
},
max(state){
let max = 0
const tasks = activeTaskList(state)
if(!tasks)
return 1
max = Math.floor(tasks.length/state.length)
if(tasks.length % state.length !== 0 || max === 0)
max += 1
return max
}
}
export default new vuex.Store({
actions,
mutations,
state,
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){
if(name === null){
console.log(name)
return null
}
if (!name.includes("[")) return name
// 截取最后一个 [ 之前的部分,然后移除所有 [...] 和 (...) 标签
const trimmed = name.substring(0, name.lastIndexOf("["))
.replace(/\s*\[[^\]]*\]\s*/g, '')
.replace(/\s*\([^\)]*\)\s*/g, '')
.trim()
// 名字以 [...] 开头时(例如 "[作者] 标题"),截取结果为空,退回原名避免整列空白
return trimmed === '' ? name.trim() : trimmed
}
function buildGalleryDownloadUrl(gid, AuthCode){
let params = new URLSearchParams({AuthCode, gid: gid.toString()})
return GalleryManageUrl + "/file/" + gid + ".zip?" + params.toString()
}
function buildGalleryManageUrl(path, params){
return GalleryManageUrl + path + "?" + new URLSearchParams(params).toString()
}
function confirmCurrentTask(state){
switch (state.category){
case 'myDownload':
state.currentTasks = state.downloadGallery
break
case 'myCollect':
state.currentTasks = state.collectGallery
break
case 'total':
state.currentTasks = state.totalGalleryTask
break
}
}
function sortTasks(state){
switch (state.sortType) {
case "name":
state.currentTasks = state.currentTasks.sort((before, after) => {
return before.name > after.name ? 1: -1
})
break
case "shortName":
state.currentTasks = state.currentTasks.sort((before, after) => {
return before.shortName > after.shortName ? 1: -1
})
break
case "createTime":
state.currentTasks = state.currentTasks.sort((before, after) => {
return before.createTime - after.createTime
})
}
}
function deleteTask(tasks, key, value){
for(let j=0; j < tasks.length; j++)
for(let i=0; i<tasks[j].length; i++)
if(tasks[j][i][key] === value){
tasks[j].splice(i, 1)
i--
break
}
}
let status = ['已提交', '下载中', '等待压缩', '压缩中', '下载完成']
// 桌面端与移动端共用同一份任务业务核心,只在分页交互上不同。
export default new vuex.Store(createGalleryStore({
variant: "desktop",
axios,
qs,
Message: ElMessage,
}))
-23
View File
@@ -1,23 +0,0 @@
function imageItem(url, index) {
return {url, src: url, index, attempt: 0, failed: false, settled: false}
}
export function startImages(urls) {
return urls.length ? [imageItem(urls[0], 0)] : []
}
export function settleImage(items, urls, item, failed = false) {
// Ignore events from images removed by a page change or dialog close.
if (!items.includes(item)) return
item.failed = failed
if (item.settled) return
item.settled = true
if (items.length < urls.length)
items.push(imageItem(urls[items.length], items.length))
}
export function retryImage(item) {
item.failed = false
item.attempt++
item.src = item.url + (item.url.includes('?') ? '&' : '?') + 'retry=' + item.attempt
}
-76
View File
@@ -1,76 +0,0 @@
export function createTaskSocket({url, onOpen, onMessage, onState,
createSocket = address => new WebSocket(address),
setTimer = setTimeout, clearTimer = clearTimeout}) {
let socket = null
let retryTimer
let connectTimer
let attempts = 0
let stopped = true
function detach() {
clearTimer(connectTimer)
connectTimer = undefined
if (socket) {
const previous = socket
socket = null
previous.onopen = previous.onclose = previous.onerror = previous.onmessage = null
try { previous.close() } catch { /* already closed */ }
}
}
function schedule() {
if (stopped) return
onState('reconnecting')
const delay = Math.min(1000 * 2 ** attempts, 30000)
attempts = Math.min(attempts + 1, 5)
clearTimer(retryTimer)
retryTimer = setTimer(connect, delay)
}
function connect() {
if (stopped) return
clearTimer(retryTimer)
retryTimer = undefined
onState(attempts ? 'reconnecting' : 'connecting')
let current
try { current = socket = createSocket(url) }
catch { schedule(); return }
const retry = () => {
if (stopped || socket !== current) return
detach()
schedule()
}
current.onopen = () => {
if (stopped || socket !== current) return
clearTimer(connectTimer)
connectTimer = undefined
attempts = 0
try { current.send('DownloaderWebsocket') }
catch { retry(); return }
onState('connected')
onOpen()
}
current.onmessage = event => {
if (!stopped && socket === current) onMessage(event)
}
current.onclose = retry
current.onerror = retry
connectTimer = setTimer(retry, 10000)
}
return {
start() {
if (!stopped) return
stopped = false
attempts = 0
connect()
},
stop() {
stopped = true
clearTimer(retryTimer)
retryTimer = undefined
detach()
onState('disconnected')
}
}
}
-27
View File
@@ -1,27 +0,0 @@
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];
}
-8
View File
@@ -1,8 +0,0 @@
export function validateLink(rawLink){
if(rawLink.trim() === "")
return false
if(rawLink.includes("hentai"))
return rawLink.includes("/g/")
else
return false
}