diff --git a/src/App.vue b/src/App.vue index 4a0b0c8..760b339 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,42 +1,71 @@ - - diff --git a/src/components/AuthScreen.vue b/src/components/AuthScreen.vue new file mode 100644 index 0000000..4a56e13 --- /dev/null +++ b/src/components/AuthScreen.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/components/DashBoard.vue b/src/components/DashBoard.vue deleted file mode 100644 index fda1d22..0000000 --- a/src/components/DashBoard.vue +++ /dev/null @@ -1,384 +0,0 @@ - - - - - diff --git a/src/components/HentaiSearch.vue b/src/components/HentaiSearch.vue deleted file mode 100644 index c19f028..0000000 --- a/src/components/HentaiSearch.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - - diff --git a/src/components/SearchView.vue b/src/components/SearchView.vue new file mode 100644 index 0000000..a638412 --- /dev/null +++ b/src/components/SearchView.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/components/SettingsView.vue b/src/components/SettingsView.vue new file mode 100644 index 0000000..97c7f7f --- /dev/null +++ b/src/components/SettingsView.vue @@ -0,0 +1,218 @@ + + + diff --git a/src/components/Side.vue b/src/components/Side.vue deleted file mode 100644 index 3f77d08..0000000 --- a/src/components/Side.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - - diff --git a/src/components/TabBar.vue b/src/components/TabBar.vue new file mode 100644 index 0000000..6fd98c7 --- /dev/null +++ b/src/components/TabBar.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/TaskDetailSheet.vue b/src/components/TaskDetailSheet.vue new file mode 100644 index 0000000..32098aa --- /dev/null +++ b/src/components/TaskDetailSheet.vue @@ -0,0 +1,200 @@ + + + diff --git a/src/components/TaskRow.vue b/src/components/TaskRow.vue new file mode 100644 index 0000000..8437267 --- /dev/null +++ b/src/components/TaskRow.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/components/TaskView.vue b/src/components/TaskView.vue new file mode 100644 index 0000000..28c389c --- /dev/null +++ b/src/components/TaskView.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/main.js b/src/main.js index a49f0fd..9e41418 100644 --- a/src/main.js +++ b/src/main.js @@ -4,5 +4,6 @@ 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') diff --git a/src/store/index.js b/src/store/index.js index 5c5636c..5ee50df 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -311,6 +311,16 @@ const mutations = { _changePage(state, targetPage){ state.page = targetPage }, + _setActiveTab(state, tab){ + state.activeTab = tab + }, + _setDetailGallery(state, gallery){ + state.detailGallery = gallery + }, + // The mobile list grows in place instead of paging through a footer control. + _showMoreTasks(state){ + state.visiblePages += 1 + }, _authed(state, data){ state.AuthCode = data.AuthCode state.userId = data.userId @@ -329,12 +339,14 @@ const mutations = { _searchLocalByLink(state, link){ let tasks = state.currentTasks let gid = link.split("/")[4] + let matched = null 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 + matched = tasks[i] name = tasks[i].name break } @@ -342,19 +354,27 @@ const mutations = { else for (let i = 0; i < tasks.length; i++) if (String(tasks[i].gid) === gid) { state.page = Math.floor(i / state.length) + 1 + matched = tasks[i] name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name break } if(!name) ElMessage("未找到此任务") - else + else { + state.searchTask.splice(0) + if(matched) + state.searchTask.push(matched) + state.isSearch = true + state.visiblePages = 1 ElMessage("已跳转到该任务所在页数,任务名:" + name) + } }, _searchLocalByKeyword(state, keyword){ state.searchTask.splice(0) if(keyword.trim() !== '') { state.page = 1 + state.visiblePages = 1 let tasks = state.currentTasks tasks.forEach((task) => { if (task.name.includes(keyword)) @@ -381,11 +401,13 @@ const mutations = { }, _setCategory(state, category){ state.category = category + state.visiblePages = 1 confirmCurrentTask(state) sortTasks(state) }, _setSortType(state, sortType){ state.sortType = sortType + state.visiblePages = 1 sortTasks(state) }, _setGalleryNameType(state, galleryNameType){ @@ -451,6 +473,9 @@ const state = { isInclude: false, //是否搜索到任务 searchTask: [], //搜索到的任务 isShowHistory: false, //是否打开面板 + activeTab: 'tasks', //移动端底部标签 tasks search settings + detailGallery: null, //详情抽屉里正在看的任务 + visiblePages: 1, //列表已展开的页数,用于“继续加载” galleryNameType: 'shortName', //名字类型 shortName name category: 'myDownload', //分类 myDownload myCollect total sortType:'shortName', //排序类型 shortName name createTime @@ -468,6 +493,18 @@ const getters = { min(){ return 1 }, + visibleTasks(state){ + return (state.currentTasks || []).slice(0, state.visiblePages * state.length) + }, + taskTotal(state){ + return (state.currentTasks || []).length + }, + taskRemaining(state, getters){ + return Math.max(0, getters.taskTotal - state.visiblePages * state.length) + }, + searchResults(state){ + return (state.searchTask || []).slice(0, state.visiblePages * state.length) + }, max(state){ let max = 0 let tasks @@ -500,8 +537,12 @@ function getShortname(name){ if (!name.includes("[")) return name // 截取最后一个 [ 之前的部分,然后移除所有 [...] 和 (...) 标签 - name = name.substring(0, name.lastIndexOf("[")) - return name.replace(/\s*\[[^\]]*\]\s*/g, '').replace(/\s*\([^\)]*\)\s*/g, '').trim() + 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){ diff --git a/src/styles/theme.css b/src/styles/theme.css new file mode 100644 index 0000000..c0f7a11 --- /dev/null +++ b/src/styles/theme.css @@ -0,0 +1,1071 @@ +/* ========== Design Tokens (mirrors the desktop build) ========== */ +:root { + --bg-body: #f1f2f5; + --bg-surface: #ffffff; + --bg-card: #ffffff; + --bg-hover: #f2f2f0; + --bg-sunken: #fafaf9; + + --text-primary: #1f2328; + --text-secondary: #6b7280; + --text-disabled: #c2c6cd; + + --border-color: #e4e4e2; + --border-strong: #d5d6d3; + --border-radius: 8px; + --border-radius-sm: 6px; + + --accent: #2563eb; + --accent-hover: #1d4ed8; + --accent-soft: #eff4ff; + + --status-success: #15803d; + --status-success-soft: #eaf7ee; + --status-warning: #b45309; + --status-warning-soft: #fdf3e3; + --status-info: #1d4ed8; + --status-info-soft: #eaf1fe; + --status-danger: #b42318; + --status-danger-soft: #fdeceb; + + --shadow-card: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06); + --shadow-elevated: 0 8px 24px rgba(16, 24, 40, 0.12); + + --transition-fast: 0.15s ease; + --transition-normal: 0.25s ease; +} + +html.dark { + --bg-body: #12141a; + --bg-surface: #191c23; + --bg-card: #191c23; + --bg-hover: #22262f; + --bg-sunken: #15181e; + + --text-primary: #e8eaee; + --text-secondary: #9aa3b2; + --text-disabled: #565d69; + + --border-color: #2a2f3a; + --border-strong: #363c49; + + --accent: #3b82f6; + --accent-hover: #60a5fa; + --accent-soft: #1c2534; + + --status-success: #4ade80; + --status-success-soft: #16281d; + --status-warning: #fbbf24; + --status-warning-soft: #2a2113; + --status-info: #60a5fa; + --status-info-soft: #172234; + --status-danger: #f87171; + --status-danger-soft: #2b1a1a; + + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.5); +} + +/* ========== Global Base ========== */ +* { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; +} + +body { + margin: 0; + padding: 0; + background-color: var(--bg-body); + color: var(--text-primary); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", + "PingFang SC", "Microsoft YaHei", Arial, sans-serif; + font-size: 15px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + transition: background-color var(--transition-normal), color var(--transition-normal); +} + +#app { + min-height: 100%; +} + +/* ========== App Shell ========== */ +.app { + display: flex; + flex-direction: column; + height: 100vh; + height: 100dvh; + overflow: hidden; + background-color: var(--bg-body); +} + +.app-views { + flex: 1; + min-height: 0; + position: relative; +} + +.view { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; +} + +.app-bar { + flex: none; + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px 10px; + background: var(--bg-surface); + border-bottom: 1px solid var(--border-color); +} + +.app-bar h1 { + margin: 0; + font-size: 17px; + font-weight: 600; +} + +.app-bar-count { + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.app-bar-spacer { + flex: 1; +} + +.connection { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; + color: var(--status-warning); + white-space: nowrap; +} + +.connection.is-connected { + color: var(--status-success); +} + +.connection-dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: currentColor; + flex: none; +} + +/* ========== Filter Bar ========== */ +.filter-bar { + flex: none; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + overflow-x: auto; + scrollbar-width: none; + background: var(--bg-body); +} + +.filter-bar::-webkit-scrollbar { + display: none; +} + +.filter { + display: inline-flex; + align-items: center; + gap: 5px; + flex: none; + height: 30px; + padding: 0 8px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-surface); +} + +.filter-label { + font-size: 11px; + color: var(--text-secondary); + flex: none; +} + +.filter-select .el-select__wrapper { + min-height: 22px; + padding: 0; + box-shadow: none; + background: transparent; +} + +.filter-select .el-select__placeholder, +.filter-select .el-select__selected-item { + font-size: 13px; + color: var(--text-primary); +} + +.filter-select-range { + width: 84px; +} + +.filter-select-sort { + width: 90px; +} + +.chip-button { + flex: none; + height: 30px; + padding: 0 12px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-surface); + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + cursor: pointer; +} + +.chip-button:disabled { + color: var(--text-disabled); + cursor: default; +} + +/* ========== Scroll Area ========== */ +.list-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding: 0 12px 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.empty-state { + padding: 56px 20px; + text-align: center; + font-size: 14px; + color: var(--text-secondary); +} + +.load-more { + flex: none; + height: 38px; + border: 1px dashed var(--border-strong); + border-radius: var(--border-radius); + background: none; + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + cursor: pointer; +} + +/* ========== Task Row ========== */ +.task-row { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) 52px; + flex: none; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-card); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.task-row-open { + min-width: 0; + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + border: 0; + background: none; + font-family: inherit; + text-align: left; + color: inherit; + cursor: pointer; +} + +.task-row-open:active { + background: var(--bg-hover); +} + +.task-thumb { + flex: none; + width: 42px; + height: 58px; + border-radius: 4px; + background: var(--bg-sunken); + overflow: hidden; +} + +.task-thumb img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.task-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.task-title { + font-size: 14px; + font-weight: 500; + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.task-meta { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.task-foot { + flex: none; + display: flex; + align-items: center; +} + +.status-pill { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} + +.status-pill.is-done { + color: var(--status-success); + background: var(--status-success-soft); +} + +.status-pill.is-running { + color: var(--status-info); + background: var(--status-info-soft); +} + +.status-pill.is-waiting { + color: var(--status-warning); + background: var(--status-warning-soft); +} + +.task-side { + display: flex; + align-items: center; + justify-content: center; + border: 0; + border-left: 1px solid var(--border-color); + background: none; + font-family: inherit; + font-size: 13px; + padding: 0; +} + +.task-side-act { + color: var(--accent); + cursor: pointer; +} + +.task-side-muted { + color: var(--text-secondary); + cursor: pointer; +} + +.task-side-muted:disabled { + color: var(--text-disabled); + cursor: default; +} + +.task-side-num { + border-left: 0; + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.task-row-edge { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: var(--bg-sunken); +} + +.task-row-edge > i { + display: block; + height: 100%; + background: var(--accent); +} + +/* ========== Search ========== */ +.segmented { + flex: none; + display: flex; + gap: 2px; + margin: 10px 12px 0; + padding: 2px; + border-radius: var(--border-radius); + background: var(--bg-sunken); + border: 1px solid var(--border-color); +} + +.segmented.is-sub { + margin: 0; +} + +.segmented button { + flex: 1; + height: 30px; + border: 0; + border-radius: var(--border-radius-sm); + background: none; + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + cursor: pointer; + white-space: nowrap; +} + +.segmented button[aria-pressed="true"] { + background: var(--bg-surface); + color: var(--text-primary); + font-weight: 500; + box-shadow: var(--shadow-card); +} + +.search-bar { + flex: none; + display: flex; + align-items: center; + gap: 8px; + height: 42px; + padding: 0 6px 0 12px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-surface); +} + +.search-bar input { + flex: 1; + min-width: 0; + border: 0; + outline: none; + background: none; + font-family: inherit; + font-size: 14px; + color: var(--text-primary); +} + +.search-bar input::placeholder { + color: var(--text-secondary); +} + +.search-bar > button { + flex: none; + height: 30px; + padding: 0 12px; + border: 0; + border-radius: var(--border-radius-sm); + background: var(--accent-soft); + color: var(--accent); + font-family: inherit; + font-size: 13px; + font-weight: 500; + cursor: pointer; +} + +.search-bar > button:disabled { + color: var(--text-disabled); + background: var(--bg-sunken); + cursor: default; +} + +.hint { + flex: none; + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 0 2px; + font-size: 12px; + color: var(--text-secondary); +} + +.hint-action { + border: 0; + background: none; + padding: 0; + font-family: inherit; + font-size: 12px; + color: var(--accent); + cursor: pointer; +} + +.remote-row { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) 58px 52px; + flex: none; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-card); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.pager-row { + flex: none; + display: flex; + gap: 8px; + padding-top: 2px; +} + +.pager-row button { + flex: 1; + height: 36px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-surface); + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + cursor: pointer; +} + +.pager-row button:disabled { + color: var(--text-disabled); + cursor: default; +} + +/* ========== Settings ========== */ +.settings-scroll { + padding-top: 10px; + gap: 8px; +} + +.group-label { + flex: none; + margin: 4px 0 -4px; + padding: 0 4px; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); +} + +.group { + flex: none; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-card); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.setting-row { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 11px 12px; + border: 0; + border-bottom: 1px solid var(--border-color); + background: none; + font-family: inherit; + font-size: 14px; + color: var(--text-primary); + text-align: left; +} + +.setting-row:last-child { + border-bottom: 0; +} + +.setting-row.is-button { + cursor: pointer; +} + +.setting-row.is-button:active { + background: var(--bg-hover); +} + +.setting-label { + flex: 1; + min-width: 0; +} + +.setting-value { + flex: none; + font-size: 14px; + color: var(--text-secondary); +} + +.setting-row.is-danger .setting-label { + color: var(--status-danger); +} + +.usage-value { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +} + +.usage-value strong { + font-size: 17px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.usage-value small { + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; +} + +.mini-button { + flex: none; + height: 28px; + padding: 0 10px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + background: none; + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + cursor: pointer; +} + +.chevron { + flex: none; + font-size: 16px; + line-height: 1; + color: var(--text-disabled); +} + +/* ========== Tab Bar ========== */ +.tab-bar { + flex: none; + display: flex; + background: var(--bg-surface); + border-top: 1px solid var(--border-color); + padding: 4px 6px calc(6px + env(safe-area-inset-bottom)); +} + +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + padding: 6px 0 4px; + border: 0; + background: none; + font-family: inherit; + font-size: 13px; + color: var(--text-secondary); + cursor: pointer; +} + +.tab-item::before { + content: ""; + width: 18px; + height: 2px; + border-radius: 999px; + background: transparent; +} + +.tab-item.is-active { + color: var(--accent); + font-weight: 600; +} + +.tab-item.is-active::before { + background: var(--accent); +} + +/* ========== Detail Sheet ========== */ +.sheet-layer { + position: fixed; + inset: 0; + z-index: 900; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.sheet-scrim { + position: absolute; + inset: 0; + border: 0; + padding: 0; + background: rgba(10, 13, 18, 0.45); + cursor: pointer; +} + +.sheet { + position: relative; + background: var(--bg-surface); + border-radius: 14px 14px 0 0; + box-shadow: var(--shadow-elevated); + max-height: 92%; + overflow-y: auto; + padding-bottom: calc(12px + env(safe-area-inset-bottom)); +} + +.sheet-grab { + display: block; + width: 34px; + height: 4px; + margin: 8px auto 0; + border-radius: 999px; + background: var(--border-strong); +} + +.sheet-head { + display: flex; + gap: 12px; + padding: 12px 14px 0; +} + +.sheet-thumb { + flex: none; + width: 54px; + height: 74px; + border-radius: var(--border-radius-sm); + background: var(--bg-sunken); + overflow: hidden; +} + +.sheet-thumb img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.sheet-heading { + flex: 1; + min-width: 0; +} + +.sheet-title { + margin: 0 0 8px; + font-size: 15px; + font-weight: 600; + line-height: 1.4; + word-break: break-word; +} + +.sheet-sub { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-secondary); +} + +.progress-line { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px 0; +} + +.progress-line .el-progress { + flex: 1; +} + +.progress-line .progress-value { + flex: none; + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.sheet-section { + margin: 16px 14px 8px; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); +} + +.sheet-actions { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + padding: 14px; +} + +.sheet-actions.is-single { + grid-template-columns: minmax(0, 1fr); + padding-bottom: 0; +} + +.sheet-action { + height: 42px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-surface); + color: var(--text-primary); + font-family: inherit; + font-size: 14px; + cursor: pointer; +} + +.sheet-action.is-primary { + background: var(--accent); + border-color: var(--accent); + color: #ffffff; +} + +.sheet-action.is-on { + color: var(--accent); + border-color: var(--accent); +} + +.sheet-action:disabled { + color: var(--text-disabled); + background: var(--bg-sunken); + border-color: var(--border-color); + cursor: default; +} + +.sheet-facts { + display: grid; + grid-template-columns: 78px minmax(0, 1fr); + gap: 0 12px; + margin: 0; + padding: 0 14px; + font-size: 13px; +} + +.sheet-facts dt { + padding: 7px 0; + color: var(--text-secondary); +} + +.sheet-facts dd { + margin: 0; + padding: 7px 0; + border-bottom: 1px solid var(--border-color); + word-break: break-word; +} + +.resolution-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 0 14px; +} + +.resolution-list button { + padding: 9px 12px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: none; + color: var(--text-secondary); + font-family: inherit; + font-size: 13px; + cursor: pointer; +} + +.resolution-list button[aria-pressed="true"] { + color: var(--accent); + border-color: var(--accent); + background: var(--accent-soft); +} + +.sheet-foot { + display: flex; + gap: 8px; + margin-top: 16px; + padding: 12px 14px 0; + border-top: 1px solid var(--border-color); +} + +.sheet-button { + flex: 1; + height: 42px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: none; + color: var(--text-secondary); + font-family: inherit; + font-size: 14px; + cursor: pointer; +} + +.sheet-button.is-accent { + background: var(--accent); + border-color: var(--accent); + color: #ffffff; +} + +.sheet-button.is-danger { + color: var(--status-danger); +} + +.sheet-button:disabled { + color: var(--text-disabled); + cursor: default; +} + +/* ========== Auth Screen ========== */ +.auth-screen { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + flex-direction: column; + justify-content: center; + padding: 24px; + background: var(--bg-body); +} + +.auth-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: var(--shadow-elevated); + padding: 28px 22px 24px; +} + +.auth-brand { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.auth-brand .auth-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 8px; + background: var(--accent); + color: #fff; + font-size: 14px; + font-weight: 700; + flex: none; +} + +.auth-brand .auth-name { + font-size: 16px; + font-weight: 600; +} + +.auth-card h1 { + margin: 0 0 6px; + font-size: 19px; + font-weight: 600; +} + +.auth-subtitle { + margin: 0 0 20px; + font-size: 13px; + color: var(--text-secondary); +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.auth-form .el-checkbox { + height: auto; +} + +/* ========== Dialogs / forms ========== */ +.section-title { + margin: 0 0 12px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.config-form .section-title { + margin-top: 20px; +} + +.config-form .section-title:first-child { + margin-top: 0; +} + +.field-hint { + margin-left: 10px; + font-size: 12px; + color: var(--text-secondary); +} + +.mono-value { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 13px; + word-break: break-all; +} + +/* ========== Element Plus harmonisation ========== */ +.el-button { + font-weight: 500; +} + +.el-button--primary { + --el-button-bg-color: var(--accent); + --el-button-border-color: var(--accent); + --el-button-hover-bg-color: var(--accent-hover); + --el-button-hover-border-color: var(--accent-hover); +} + +.el-dialog { + --el-dialog-margin-top: 8vh; + border-radius: 12px; +} + +/* Very narrow phones: shrink the filter controls instead of clipping them. */ +@media (max-width: 360px) { + .filter-select-range { + width: 76px; + } + + .filter-select-sort { + width: 82px; + } + + .filter, + .chip-button { + padding: 0 6px; + } + + .app-bar { + padding: 10px 12px 8px; + } +} diff --git a/src/utils/thumbnail.js b/src/utils/thumbnail.js new file mode 100644 index 0000000..b5f9e52 --- /dev/null +++ b/src/utils/thumbnail.js @@ -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]; +} diff --git a/tests/thumbnail.test.mjs b/tests/thumbnail.test.mjs new file mode 100644 index 0000000..8db1996 --- /dev/null +++ b/tests/thumbnail.test.mjs @@ -0,0 +1,47 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import {ehThumbnailUrl, pickDefaultResolution} from '../src/utils/thumbnail.js' + +test('remote thumbnail paths are wrapped with the auth code', () => { + const url = ehThumbnailUrl('samples/thumb.png', 'abc123') + assert.match(url, /^https:\/\/downloader\.lionwebsite\.xyz\/GalleryManage\/ehThumbnail\?/) + assert.match(url, /path=samples%2Fthumb\.png/) + assert.match(url, /AuthCode=abc123/) +}) + +test('a missing thumbnail path produces no url', () => { + assert.equal(ehThumbnailUrl('', 'abc123'), '') + assert.equal(ehThumbnailUrl(undefined, 'abc123'), '') +}) + +test('the original resolution is preselected when offered', () => { + const chosen = pickDefaultResolution({ + '800x': '37.53MiB', + '1280x': '66.57MiB', + '2560x': '84.01MiB', + 'Original': '498.2MiB', + }) + assert.equal(chosen, 'Original') +}) + +test('the original label is matched case-insensitively', () => { + assert.equal(pickDefaultResolution({'800x': '1MiB', 'original': '2MiB'}), 'original') +}) + +test('without an original the largest entry wins across units', () => { + const chosen = pickDefaultResolution({ + '800x': '37.53MiB', + '2560x': '84.01MiB', + '2160x': '1.2GiB', + }) + assert.equal(chosen, '2160x') +}) + +test('an empty or absent resolution map selects nothing', () => { + assert.equal(pickDefaultResolution({}), '') + assert.equal(pickDefaultResolution(undefined), '') +}) + +test('unparseable sizes fall back to the first entry instead of throwing', () => { + assert.equal(pickDefaultResolution({'800x': 'unknown', '1280x': 'unknown'}), '800x') +})