Skip to content

Commit

Permalink
feat: title replacement
Browse files Browse the repository at this point in the history
close #47
  • Loading branch information
Tsuk1ko committed Apr 14, 2024
1 parent 49000b0 commit 9344944
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 44 deletions.
3 changes: 3 additions & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
本子详情页会默认变为在新选项卡中打开,如果不喜欢就关掉它
- **压缩文件名**
默认是`{{japanese}}.zip`,可自定义下载下来的压缩文件的文件名,包括扩展名,比如`{{english}}.cbz`
如果压缩文件名中含有文件名非法字符则会自动被替换成空格,你也可以利用「标题替换」设置进行自定义替换
支持的占位符:
- `{{english}}` - 本子英文名
- `{{japanese}}` - 本子日文名
Expand Down Expand Up @@ -88,6 +89,8 @@
在 Safari 和 Firefox 上无法使用,且下载速度**极慢**,因此不推荐启用除非真的有内存消耗问题
- **阻止控制台清空**
只在 nHentai 官方站点可用(需要),当你需要提交控制台日志来定位问题的时候请启用它
- **标题替换**
可以对压缩文件名中的标题进行字符替换,支持正则表达式

## 其他功能

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Support Tampermonkey and Violentmonkey. Not and will not support Greasemonkey.
Gallery page will be open on a new window by default, turn off it if you don't like it.
- **Compression filename**
Default is `{{japanese}}.zip`. You can custom the naming of downloaded compression file, including the file extension, such as `{{english}}.cbz`.
If the compressed filename contains illegal characters, they will be automatically replaced with spaces. You can also use the *Title replacement* setting to perform custom replacement.
Available placeholders:
- `{{english}}` - English name of manga
- `{{japanese}}` - Japanese name of manga
Expand Down Expand Up @@ -91,6 +92,8 @@ Support Tampermonkey and Violentmonkey. Not and will not support Greasemonkey.
But this not work on Safari and Firefox, and the download process is **extremely slow**. So not recommended unless you really have memory usage issues.
- **Prevent console clearing**
Only available on nHentai official site. It is useful when you need to submit console log for debugging.
- **Title replacement**
Character replacement can be performed on the title in the compressed filename, and regular expressions are supported.

## Other features

Expand Down
87 changes: 72 additions & 15 deletions src/app/SettingsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@
<!-- 添加元数据文件 -->
<el-form-item :label="t('setting.addMetaFile')">
<el-checkbox-group v-model="settings.addMetaFile">
<el-checkbox label="ComicInfoXml">ComicInfo.xml</el-checkbox>
<el-checkbox label="EzeInfoJson">info.json (eze)</el-checkbox>
<el-checkbox label="ComicInfo.xml" value="ComicInfoXml" />
<el-checkbox label="info.json (eze)" value="EzeInfoJson" />
</el-checkbox-group>
</el-form-item>
<!-- 元数据标题语言 -->
Expand Down Expand Up @@ -163,6 +163,49 @@
>
<el-switch v-model="settings.preventConsoleClearing" />
</el-form-item>
<el-collapse>
<el-collapse-item>
<template #title>
<span style="color: var(--el-text-color-regular)">{{
t('setting.titleReplacement')
}}</span>
</template>
<el-table id="title-replacement-table" :data="settings.titleReplacement">
<el-table-column label="From">
<template #default="scope">
<el-input v-model="scope.row.from">
<template #prefix>
<span v-if="scope.row.regexp" class="no-sl">/</span>
</template>
<template #suffix>
<span v-if="scope.row.regexp" class="no-sl">/</span>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="To">
<template #default="scope">
<el-input v-model="scope.row.to" />
</template>
</el-table-column>
<el-table-column label="RegExp" width="80">
<template #default="scope">
<el-switch v-model="scope.row.regexp" />
</template>
</el-table-column>
<el-table-column width="70">
<template #default="scope">
<ConfirmPopup @confirm="() => delTitleReplacement(scope.$index)">
<el-button type="danger" :icon="Delete" />
</ConfirmPopup>
</template>
</el-table-column>
<template #append>
<el-button text style="width: 100%" @click="addTitleReplacement">+</el-button>
</template>
</el-table>
</el-collapse-item>
</el-collapse>
</el-form>
<el-divider>{{ t('setting.history.title') }}</el-divider>
<p class="no-sl">
Expand All @@ -183,19 +226,11 @@
<el-button type="primary" :icon="Upload" :loading="importing" @click="importHistory">{{
t('setting.history.import')
}}</el-button>
<el-popconfirm
:title="t('setting.history.clearConfirm')"
:confirm-button-text="t('setting.history.clearConfirmYes')"
:cancel-button-text="t('setting.history.clearConfirmNo')"
placement="top"
@confirm="clearHistory"
>
<template #reference>
<el-button type="danger" :icon="Delete" :loading="clearing">{{
t('setting.history.clear')
}}</el-button>
</template>
</el-popconfirm>
<ConfirmPopup @confirm="clearHistory">
<el-button type="danger" :icon="Delete" :loading="clearing">{{
t('setting.history.clear')
}}</el-button>
</ConfirmPopup>
<p class="no-sl">{{ t('setting.history.importTip') }}</p>
</div>
</el-dialog>
Expand All @@ -206,6 +241,7 @@ import { GM_openInTab } from '$';
import { computed, ref, watch } from 'vue';
import { Delete, Download, Upload } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import ConfirmPopup from '@/components/ConfirmPopup.vue';
import {
DISABLE_STREAM_DOWNLOAD,
nHentaiDownloadHosts,
Expand Down Expand Up @@ -314,6 +350,14 @@ const clearHistory = async () => {
showMessageBySucceed(succeed);
};
const addTitleReplacement = () => {
settings.titleReplacement.push({ from: '', to: '', regexp: false });
};
const delTitleReplacement = (index: number) => {
settings.titleReplacement.splice(index, 1);
};
watch(
() => settings.language,
val => {
Expand Down Expand Up @@ -386,5 +430,18 @@ defineExpose({ open });
.el-form-item__label {
user-select: none;
}
.el-table {
.el-input__prefix,
.el-input__suffix {
line-height: 30px;
}
}
.el-table__empty-block {
display: none;
}
}
.el-select-dropdown {
user-select: none;
}
</style>
20 changes: 20 additions & 0 deletions src/components/ConfirmPopup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<el-popconfirm
:title="t('confirmPopup.title')"
:confirm-button-text="t('confirmPopup.yes')"
:cancel-button-text="t('confirmPopup.no')"
placement="top"
@confirm="(...args) => emit('confirm', ...args)"
>
<template #reference>
<slot></slot>
</template>
</el-popconfirm>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['confirm']);
const { t } = useI18n();
</script>
9 changes: 6 additions & 3 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,13 @@ export default {
nHentaiDownloadHost: 'nHentai download host',
addMetaFile: 'Add metadata file',
metaFileTitleLanguage: 'Title language',
titleReplacement: 'Title replacement',
history: {
title: 'Download History',
downloadedNumberTip: 'You have downloaded {num} manga on this site using nHentai Helper.',
import: 'Import',
export: 'Export',
clear: 'Clear',
clearConfirm: 'Are you sure?',
clearConfirmYes: '', // empty will be default text 'Yes'
clearConfirmNo: '', // empty will be default text 'No'
importTip: 'Tip: Import will not clear the existing history, but merges with it.',
},
},
Expand Down Expand Up @@ -80,4 +78,9 @@ export default {
downloading: 'Downloading',
compressing: 'Compressing',
},
confirmPopup: {
title: 'Are you sure?',
yes: '', // empty will be default text 'Yes'
no: '', // empty will be default text 'No'
},
};
9 changes: 6 additions & 3 deletions src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,13 @@ export default {
nHentaiDownloadHost: 'nHentai 下载节点',
addMetaFile: '添加元数据文件',
metaFileTitleLanguage: '标题语言',
titleReplacement: '标题替换',
history: {
title: '下载历史',
downloadedNumberTip: '你在这个站点上已经用 nHentai 助手下载了 {num} 个本子',
import: '导入',
export: '导出',
clear: '清空',
clearConfirm: '真的吗?',
clearConfirmYes: '真的',
clearConfirmNo: '手滑了',
importTip: '提示:导入会将导入的历史记录与现有历史记录合并,不会清空现有历史记录',
},
},
Expand All @@ -76,4 +74,9 @@ export default {
downloading: '下载中',
compressing: '压缩中',
},
confirmPopup: {
title: '真的吗?',
yes: '真的',
no: '算了',
},
};
24 changes: 20 additions & 4 deletions src/utils/nhentai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import $ from 'jquery';
import { filter, invert, map, once, sample } from 'lodash-es';
import { checkHost, fetchJSON, getText } from './request';
import { compileTemplate } from './common';
import { NHentaiDownloadHostSpecial, nHentaiDownloadHosts, settings } from './settings';
import {
NHentaiDownloadHostSpecial,
nHentaiDownloadHosts,
settings,
validTitleReplacement,
} from './settings';
import logger from './logger';
import { Counter } from './counter';
import { loadHTML } from './html';
Expand Down Expand Up @@ -242,9 +247,9 @@ export const getGalleryInfo = async (gid?: number | string): Promise<NHentaiGall
title,
pages: infoPages,
cfName: compileTemplate(settings.compressionFilename)({
english: english || japanese,
japanese: japanese || english,
pretty: pretty || english || japanese,
english: applyTitleReplacement(english || japanese),
japanese: applyTitleReplacement(japanese || english),
pretty: applyTitleReplacement(pretty || english || japanese),
id,
pages: num_pages,
artist: getCFNameArtists(tags),
Expand Down Expand Up @@ -299,3 +304,14 @@ const getMediaUrlTemplate = async () => {
};

const getCompliedMediaUrlTemplate = once(async () => compileTemplate(await getMediaUrlTemplate()));

const applyTitleReplacement = (title: string) => {
if (!validTitleReplacement.value.length) return title;
return validTitleReplacement.value.reduce((pre, { from, to, regexp }) => {
try {
return pre.replaceAll(regexp ? new RegExp(from, 'g') : from, to);
} catch {
return pre;
}
}, title);
};
76 changes: 57 additions & 19 deletions src/utils/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GM_getValue, GM_setValue } from '$';
import { toRaw, reactive, toRefs, watch } from 'vue';
import { toRaw, reactive, toRefs, watch, computed } from 'vue';
import type { Ref } from 'vue';

import { each, intersection, isEqual, mapValues, once } from 'lodash-es';
Expand Down Expand Up @@ -67,6 +67,8 @@ export interface Settings {
addMetaFile: string[];
/** 元数据标题语言 */
metaFileTitleLanguage: string;
/** 标题替换 */
titleReplacement: Array<{ from: string; to: string; regexp: boolean }>;
}

type SettingValidator = (val: any) => boolean;
Expand All @@ -76,6 +78,7 @@ interface SettingDefinition<T> {
key: string;
default: T extends any[] | Record<any, any> ? () => T : T;
validator: SettingValidator;
itemValidator?: SettingValidator;
formatter?: SettingFormatter<T>;
}

Expand Down Expand Up @@ -210,14 +213,24 @@ export const settingDefinitions: Readonly<{
addMetaFile: {
key: 'add_meta_file',
default: () => [],
validator: val => Array.isArray(val) && val.every(stringValidator),
validator: val => Array.isArray(val),
formatter: val => intersection(val, availableMetaFiles),
},
metaFileTitleLanguage: {
key: 'meta_file_title_language',
default: 'english',
validator: val => availableMetaFileTitleLanguage.has(val),
},
titleReplacement: {
key: 'title_replacement',
default: () => [],
validator: val => Array.isArray(val),
itemValidator: item =>
item &&
stringValidator(item.from) &&
stringValidator(item.to) &&
booleanValidator(item.regexp),
},
};

const browserDetect = detect();
Expand All @@ -240,24 +253,49 @@ export const startWatchSettings = once(() => {
const settingRefs = toRefs(writeableSettings);
each(settingRefs, (ref, key) => {
const cur = settingDefinitions[key as keyof Settings] as SettingDefinition<any>;
watch(ref as Ref<any>, val => {
if (!cur.validator(val)) {
ref.value = typeof cur.default === 'function' ? cur.default() : cur.default;
return;
}
if (cur.formatter) {
const formattedVal = cur.formatter(val);
if (
typeof formattedVal === 'object'
? !isEqual(ref.value, formattedVal)
: ref.value !== formattedVal
) {
ref.value = formattedVal;
return;
}
}
let valChanged = false;
const saveValue = (val: any) => {
logger.log('update setting', cur.key, toRaw(val));
GM_setValue(cur.key, val);
});
};
watch(
ref as Ref<any>,
val => {
if (valChanged) {
valChanged = false;
saveValue(val);
return;
}
const applyChange = (newVal: any) => {
val = newVal;
ref.value = newVal;
valChanged = true;
};
if (!cur.validator(val)) {
applyChange(typeof cur.default === 'function' ? cur.default() : cur.default);
return;
}
if (Array.isArray(val) && cur.itemValidator) {
const validItems = val.filter(cur.itemValidator);
if (val.length !== validItems.length) {
applyChange(validItems);
}
}
if (cur.formatter) {
const formattedVal = cur.formatter(val);
if (
typeof formattedVal === 'object' ? !isEqual(val, formattedVal) : val !== formattedVal
) {
applyChange(formattedVal);
}
}
if (!valChanged) saveValue(val);
},
typeof ref.value === 'object' ? { deep: true } : undefined,
);
});
});

export const validTitleReplacement = computed(() =>
settings.titleReplacement.filter(item => item?.from),
);

0 comments on commit 9344944

Please sign in to comment.