feat(webui): add UI setting to group series using japanese characters

Closes: #1715
This commit is contained in:
Gauthier Roebroeck 2025-02-10 13:39:55 +08:00
parent 7f55fe152b
commit 6c71e07a27
4 changed files with 150 additions and 8 deletions

View file

@ -1034,10 +1034,17 @@
"more": "More titles"
},
"ui_settings": {
"general": "General",
"label_oauth2_auto_login": "Automatic OAuth2 login",
"label_oauth2_hide_login": "Hide login fields if OAuth2 is enabled",
"label_poster_blur_unread": "Blur poster for unread books and series",
"label_poster_stretch": "Stretch poster to fit card",
"label_series_groups": "Series grouping",
"section_oauth2": "OAuth2",
"series_groups": {
"alpha": "Alphabetical",
"japanese": "Gojūon (Japanese)"
},
"tooltip_oauth2_auto_login": "Requires a single OAuth2 provider, and 'hide login fields' enabled"
},
"updates": {

View file

@ -18,6 +18,7 @@ export enum CLIENT_SETTING {
WEBUI_POSTER_STRETCH = 'webui.poster.stretch',
WEBUI_POSTER_BLUR_UNREAD = 'webui.poster.blur_unread',
WEBUI_LIBRARIES = 'webui.libraries',
WEBUI_SERIES_GROUPS = 'webui.series_groups',
}
export interface ClientSettingLibrary {
@ -29,3 +30,85 @@ export interface ClientSettingLibraryUpdate {
libraryId: string,
patch: ClientSettingLibrary,
}
export interface ClientSettingsSeriesGroup {
name: string,
groups: Record<string, string[]>
}
export const SERIES_GROUP_ALPHA = {
name: 'alpha',
groups: {
'A': ['A'],
'B': ['B'],
'C': ['C'],
'D': ['D'],
'E': ['E'],
'F': ['F'],
'G': ['G'],
'H': ['H'],
'I': ['I'],
'J': ['J'],
'K': ['K'],
'L': ['L'],
'M': ['M'],
'N': ['N'],
'O': ['O'],
'P': ['P'],
'Q': ['Q'],
'R': ['R'],
'S': ['S'],
'T': ['T'],
'U': ['U'],
'V': ['V'],
'W': ['W'],
'X': ['X'],
'Y': ['Y'],
'Z': ['Z'],
},
} as ClientSettingsSeriesGroup
export const SERIES_GROUP_JAPANESE = {
name: 'japanese',
groups: {
'あ': ['あ', 'ア'],
'い': ['い', 'イ'],
'う': ['う', 'ゔ', 'ウ', 'ヴ'],
'え': ['え', 'エ'],
'お': ['お', 'オ'],
'か': ['か', 'が', 'カ', 'ガ'],
'き': ['き', 'ぎ', 'キ', 'ギ'],
'く': ['く', 'ぐ', 'ク', 'グ'],
'け': ['け', 'げ', 'ケ', 'ゲ'],
'こ': ['こ', 'ご', 'コ', 'ゴ'],
'さ': ['さ', 'ざ', 'サ', 'ザ'],
'し': ['し', 'じ', 'シ', 'ジ'],
'す': ['す', 'ず', 'ス', 'ズ'],
'せ': ['せ', 'ぜ', 'セ', 'ゼ'],
'そ': ['そ', 'ぞ', 'ソ', 'ゾ'],
'た': ['た', 'だ', 'タ', 'ダ'],
'ち': ['ち', 'ぢ', 'チ', 'ヂ'],
'つ': ['つ', 'づ', 'ツ', 'ズ'],
'て': ['て', 'で', 'テ', 'デ'],
'と': ['と', 'ど', 'ト', 'ド'],
'は': ['は', 'ば', 'ぱ', 'ハ', 'バ', 'パ'],
'ひ': ['ひ', 'び', 'ぴ', 'ヒ', 'ビ', 'ピ'],
'ふ': ['ふ', 'ぶ', 'ぷ', 'フ', 'ブ', 'プ'],
'へ': ['へ', 'べ', 'ぺ', 'ヘ', 'ベ', 'ベ'],
'ほ': ['ほ', 'ぼ', 'ぽ', 'ホ', 'ボ', 'ポ'],
'ま': ['ま', 'マ'],
'み': ['み', 'ミ'],
'む': ['む', 'ム'],
'め': ['め', 'メ'],
'も': ['も', 'モ'],
'や': ['や', 'ヤ'],
'ゆ': ['ゆ', 'ユ'],
'よ': ['よ', 'ヨ'],
'ら': ['ら', 'ラ'],
'り': ['り', 'リ'],
'る': ['る', 'ル'],
'れ': ['れ', 'レ'],
'ろ': ['ろ', 'ロ'],
'わ': ['わ', 'ワ'],
},
} as ClientSettingsSeriesGroup

View file

@ -74,7 +74,7 @@
<v-container fluid>
<alphabetical-navigation
class="text-center"
:symbols="alphabeticalNavigation"
:symbols="seriesGroupingKeys"
:selected="selectedSymbol"
:group-count="seriesGroups"
@clicked="filterByStarting"
@ -209,6 +209,7 @@ import {
NameValue,
} from '@/types/filter'
import LibrariesActionsMenu from '@/components/menus/LibrariesActionsMenu.vue'
import {CLIENT_SETTING, ClientSettingsSeriesGroup, SERIES_GROUP_ALPHA} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'BrowseLibraries',
@ -231,7 +232,6 @@ export default Vue.extend({
return {
series: [] as SeriesDto[],
seriesGroups: [] as GroupCountDto[],
alphabeticalNavigation: ['ALL', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
selectedSymbol: 'ALL',
selectedSeries: [] as SeriesDto[],
page: 1,
@ -326,6 +326,21 @@ export default Vue.extend({
next()
},
computed: {
seriesGrouping(): Record<string, string[]> {
let s: Record<string, string[]>
try {
s = (JSON.parse(this.$store.getters.getClientSettings[CLIENT_SETTING.WEBUI_SERIES_GROUPS].value) as ClientSettingsSeriesGroup).groups
} catch (_) {
s = SERIES_GROUP_ALPHA.groups
}
return s
},
seriesGroupingKeys(): string[] {
return ['ALL', '#', ...this.$_.keys(this.seriesGrouping)]
},
seriesGroupingValues(): string[] {
return this.$_(this.seriesGrouping).values().flatten().map(this.$_.lowerCase).toArray() as unknown as string[]
},
library(): LibraryDto | undefined {
return this.getLibraryLazy(this.libraryId)
},
@ -337,11 +352,13 @@ export default Vue.extend({
symbolCondition(): SearchConditionSeries | undefined {
if (this.selectedSymbol === 'ALL') return undefined
if (this.selectedSymbol === '#') return new SearchConditionAllOfSeries(
this.alphabeticalNavigation
.filter(it => it !== 'ALL' && it !== '#')
this.seriesGroupingValues
.map(it => new SearchConditionTitleSort(new SearchOperatorDoesNotBeginWith(it))),
)
return new SearchConditionTitleSort(new SearchOperatorBeginsWith(this.selectedSymbol))
return new SearchConditionAnyOfSeries(
this.seriesGrouping[this.selectedSymbol]
.map(it => new SearchConditionTitleSort(new SearchOperatorBeginsWith(it))),
)
},
itemContext(): ItemContext[] {
if (this.sortActive.key === 'booksMetadata.releaseDate') return [ItemContext.RELEASE_DATE]
@ -737,11 +754,11 @@ export default Vue.extend({
condition: new SearchConditionAllOfSeries(groupConditions),
} as SeriesSearch)
const nonAlpha = seriesGroups
.filter((g) => !(/[a-zA-Z]/).test(g.group))
.filter((g) => !this.seriesGroupingValues.includes(g.group))
.reduce((a, b) => a + b.count, 0)
const all = seriesGroups.reduce((a, b) => a + b.count, 0)
this.seriesGroups = [
...seriesGroups.filter((g) => (/[a-zA-Z]/).test(g.group)),
...seriesGroups.filter((g) => this.seriesGroupingValues.includes(g.group)),
{group: '#', count: nonAlpha} as GroupCountDto,
{group: 'ALL', count: all} as GroupCountDto,
]

View file

@ -2,6 +2,23 @@
<v-container fluid class="pa-6">
<v-row>
<v-col cols="auto">
<span class="font-weight-black text-h6">{{ $t('ui_settings.general') }}</span>
<v-radio-group
v-model="form.seriesGroups"
@change="$v.form.seriesGroups.$touch()"
:label="$t('ui_settings.label_series_groups')"
hide-details
>
<v-radio value="alpha" :label="$t('ui_settings.series_groups.alpha')"/>
<v-radio value="japanese" :label="$t('ui_settings.series_groups.japanese')"/>
</v-radio-group>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<span class="font-weight-black text-h6">{{ $t('ui_settings.section_oauth2') }}</span>
<v-checkbox
v-model="form.oauth2HideLogin"
@change="$v.form.oauth2HideLogin.$touch()"
@ -48,7 +65,13 @@
<script lang="ts">
import Vue from 'vue'
import {CLIENT_SETTING, ClientSettingGlobalUpdateDto} from '@/types/komga-clientsettings'
import {
CLIENT_SETTING,
ClientSettingGlobalUpdateDto,
ClientSettingsSeriesGroup,
SERIES_GROUP_ALPHA,
SERIES_GROUP_JAPANESE,
} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'UISettings',
@ -56,12 +79,14 @@ export default Vue.extend({
form: {
oauth2HideLogin: false,
oauth2AutoLogin: false,
seriesGroups: 'alpha',
},
}),
validations: {
form: {
oauth2HideLogin: {},
oauth2AutoLogin: {},
seriesGroups: {},
},
},
mounted() {
@ -80,6 +105,10 @@ export default Vue.extend({
await this.$store.dispatch('getClientSettingsGlobal')
this.form.oauth2HideLogin = this.$store.state.komgaSettings.clientSettingsGlobal[CLIENT_SETTING.WEBUI_OAUTH2_HIDE_LOGIN]?.value === 'true'
this.form.oauth2AutoLogin = this.$store.state.komgaSettings.clientSettingsGlobal[CLIENT_SETTING.WEBUI_OAUTH2_AUTO_LOGIN]?.value === 'true'
try {
this.form.seriesGroups = (JSON.parse(this.$store.state.komgaSettings.clientSettingsGlobal[CLIENT_SETTING.WEBUI_SERIES_GROUPS]?.value) as ClientSettingsSeriesGroup)?.name
} catch (_) {
}
this.$v.form.$reset()
},
async saveSettings() {
@ -96,6 +125,12 @@ export default Vue.extend({
allowUnauthorized: true,
}
if (this.$v.form?.seriesGroups?.$dirty)
newSettings[CLIENT_SETTING.WEBUI_SERIES_GROUPS] = {
value: JSON.stringify(this.form.seriesGroups === 'alpha' ? SERIES_GROUP_ALPHA : SERIES_GROUP_JAPANESE),
allowUnauthorized: false,
}
await this.$komgaSettings.updateClientSettingGlobal(newSettings)
await this.refreshSettings()