feat: 增加手动整理集数定位规则配置并支持智能生成集数定位模板 (#473)

This commit is contained in:
Album
2026-05-19 07:20:23 +08:00
committed by GitHub
parent cf139a938e
commit de3523056a
5 changed files with 521 additions and 9 deletions

View File

@@ -89,6 +89,30 @@ const previewLoaded = ref(false)
// 预览数据
const previewData = ref<ManualTransferPreviewData>()
interface EpisodeFormatRecommendData {
rule_name?: string
rule_index?: number
pattern?: string
episode_format?: string
sample_file?: string
min_file_size_mb?: number
message?: string
}
const episodeFormatRecommendState = reactive<{
loading: boolean
ruleName?: string
sampleFile?: string
lastMessage?: string
}>({
loading: false,
ruleName: undefined,
sampleFile: undefined,
lastMessage: undefined,
})
const episodeFormatRuleConfigured = ref<boolean | undefined>(undefined)
function getFileItemKey(item?: FileItem) {
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
}
@@ -414,6 +438,40 @@ const previewToggleIcon = computed(() => {
return previewVisible.value ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
})
const episodeFormatRecommendSourceItem = computed<FileItem | undefined>(() => {
if (transferForm.fileitem?.path) return transferForm.fileitem
if (normalizedItems.value.length !== 1) return undefined
return normalizedItems.value[0]
})
const canRecommendEpisodeFormat = computed(() => {
return (
Boolean(episodeFormatRecommendSourceItem.value?.path) &&
!progressDialog.value &&
!episodeFormatRecommendState.loading
)
})
const episodeFormatRecommendTooltip = computed(() => {
if (episodeFormatRecommendState.loading) return t('dialog.reorganize.episodeFormatRecommendLoading')
if (!episodeFormatRecommendSourceItem.value?.path) return t('dialog.reorganize.episodeFormatRecommendSelectFile')
if (episodeFormatRuleConfigured.value === false) return t('dialog.reorganize.episodeFormatRecommendNeedWords')
return t('dialog.reorganize.episodeFormatRecommendAction')
})
watch(
() => getFileItemKey(episodeFormatRecommendSourceItem.value),
sourceKey => {
transferForm.fileitem = episodeFormatRecommendSourceItem.value ?? ({} as FileItem)
if (!sourceKey) {
episodeFormatRecommendState.ruleName = undefined
episodeFormatRecommendState.sampleFile = undefined
episodeFormatRecommendState.lastMessage = undefined
}
},
{ immediate: true },
)
// 构造整理请求
function createTransferPayload(options: { item?: FileItem; logid?: number; preview?: boolean }) {
const payload: ManualTransferPayload = {
@@ -434,6 +492,65 @@ async function requestManualTransfer<T = any>(
return await api.post(`transfer/manual?background=${background}`, payload)
}
async function loadEpisodeFormatRuleConfiguration() {
try {
const result: { [key: string]: any } = await api.get('system/setting/EpisodeFormatRuleTable')
episodeFormatRuleConfigured.value = Boolean(result.data?.value?.length)
} catch (error) {
console.log(error)
episodeFormatRuleConfigured.value = undefined
}
}
async function handleRecommendEpisodeFormat() {
const sourceItem = episodeFormatRecommendSourceItem.value
if (!sourceItem?.path) {
$toast.warning(t('dialog.reorganize.episodeFormatRecommendSelectFile'))
return
}
if (episodeFormatRuleConfigured.value === false) {
$toast.warning(t('dialog.reorganize.episodeFormatRecommendNeedWords'))
return
}
episodeFormatRecommendState.loading = true
try {
const hasExistingEpisodeFormat = Boolean(transferForm.episode_format?.trim())
const result = await api.post('transfer/episode-format/recommend', {
fileitem: sourceItem,
})
if (!result.success) {
$toast.error(result.message || t('dialog.reorganize.episodeFormatRecommendFailed'))
return
}
const data = (result.data ?? {}) as EpisodeFormatRecommendData
if (!data.episode_format) {
$toast.error(t('dialog.reorganize.episodeFormatRecommendFailed'))
return
}
transferForm.episode_format = data.episode_format
episodeFormatRecommendState.ruleName = data.rule_name
episodeFormatRecommendState.sampleFile = data.sample_file
episodeFormatRecommendState.lastMessage = data.message
$toast.success(
hasExistingEpisodeFormat
? t('dialog.reorganize.episodeFormatRecommendOverwriteSuccess')
: t('dialog.reorganize.episodeFormatRecommendSuccess'),
)
} catch (error: any) {
console.log(error)
$toast.error(error?.message || t('dialog.reorganize.episodeFormatRecommendFailed'))
} finally {
episodeFormatRecommendState.loading = false
}
}
// 默认预览数据
function getDefaultPreviewData(): ManualTransferPreviewData {
return {
@@ -769,6 +886,7 @@ async function transfer(background: boolean = false) {
onMounted(() => {
loadDirectories()
loadStorages()
loadEpisodeFormatRuleConfiguration()
})
onUnmounted(() => {
@@ -778,8 +896,15 @@ onUnmounted(() => {
</script>
<template>
<VDialog :scrollable="!previewVisible || !display.mdAndUp.value" :max-width="dialogMaxWidth" :fullscreen="!display.mdAndUp.value">
<VCard class="reorganize-dialog-card" :class="{ 'reorganize-dialog-card--split': previewVisible && display.mdAndUp.value }">
<VDialog
:scrollable="!previewVisible || !display.mdAndUp.value"
:max-width="dialogMaxWidth"
:fullscreen="!display.mdAndUp.value"
>
<VCard
class="reorganize-dialog-card"
:class="{ 'reorganize-dialog-card--split': previewVisible && display.mdAndUp.value }"
>
<VCardItem class="py-2">
<template #prepend> <VIcon icon="mdi-folder-move" class="me-2" /> </template>
<VCardTitle>{{ dialogTitle }}</VCardTitle>
@@ -914,7 +1039,29 @@ onUnmounted(() => {
:hint="t('dialog.reorganize.episodeFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
>
<template #append-inner>
<VTooltip location="top">
<template #activator="{ props: tooltipProps }">
<IconBtn
v-bind="tooltipProps"
type="button"
color="primary"
variant="text"
size="small"
class="ms-1"
icon="mdi-auto-fix"
:loading="episodeFormatRecommendState.loading"
:disabled="!canRecommendEpisodeFormat"
@click.stop="handleRecommendEpisodeFormat"
/>
</template>
<span>
{{ episodeFormatRecommendTooltip }}
</span>
</VTooltip>
</template>
</VTextField>
</VCol>
<VCol cols="12" md="6">
<VTextField
@@ -1162,9 +1309,9 @@ onUnmounted(() => {
.reorganize-dialog-card--split .reorganize-dialog-card__body {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1 1 auto;
flex-direction: column;
}
.reorganize-dialog-card--split .reorganize-main-row {
@@ -1177,8 +1324,8 @@ onUnmounted(() => {
overflow: hidden;
align-items: stretch;
grid-template-columns: minmax(0, 1fr);
min-block-size: 0;
inline-size: 100%;
min-block-size: 0;
transition: grid-template-columns 0.25s ease;
}
@@ -1190,8 +1337,8 @@ onUnmounted(() => {
display: flex;
overflow: hidden;
flex-direction: column;
min-block-size: 0;
max-inline-size: none;
min-block-size: 0;
min-inline-size: 0;
}
@@ -1264,9 +1411,9 @@ onUnmounted(() => {
.reorganize-preview-pane__body {
display: flex;
overflow: hidden;
flex: 1 1 auto;
flex-direction: column;
overflow: hidden;
min-block-size: 0;
}
@@ -1493,8 +1640,8 @@ onUnmounted(() => {
@media (width <= 959px) {
.reorganize-dialog-card,
.reorganize-dialog-card--split {
max-block-size: none;
block-size: auto;
max-block-size: none;
}
.reorganize-dialog-card--split .reorganize-dialog-card__body,

View File

@@ -1756,7 +1756,8 @@ export default {
siteDataRefreshInterval: 'Site Data Refresh Interval',
siteDataRefreshIntervalHint: 'Time interval for refreshing site user upload/download data',
searchResourcePages: 'Search Resource Pages',
searchResourcePagesHint: 'Number of consecutive pages to fetch from the current page when searching site resources. Default is 1.',
searchResourcePagesHint:
'Number of consecutive pages to fetch from the current page when searching site resources. Default is 1.',
readSiteMessage: 'Read Site Messages',
readSiteMessageHint: 'Read site messages and send notifications when refreshing data',
siteReset: 'Site Reset',
@@ -1868,6 +1869,29 @@ export default {
excludeWordsHint: 'Support regular expressions, special characters need \\ escape, one line for each block word',
excludeWordsSaveSuccess: 'File organization block words saved successfully',
excludeWordsSaveFailed: 'Failed to save file organization block words!',
episodeFormatRule: 'Manual Reorganize Episode Format Rules',
episodeFormatRuleDesc: 'Extract episode number from filename via regex.',
episodeFormatRuleName: 'Rule Name',
episodeFormatRuleNameHint: 'Enter the rule name',
episodeFormatRulePattern: 'Regular Expression',
episodeFormatRulePatternHint: 'Must contain (?&lt;ep&gt;...) named group',
episodeFormatRuleMinSize: 'Min Size (MB)',
episodeFormatRuleMinSizeHint: 'Files smaller than this will be ignored',
episodeFormatRuleGuideTitle: 'Tips',
episodeFormatRuleGuideContent:
'Rules are matched from top to bottom, and the first matched rule will be used.\n' +
'Write matching rules based on the actual filename structure:\n' +
'Match fixed text directly, and mark the episode number with the named group (?&lt;ep&gt;...);\n' +
'For other variable but position-sensitive parts, use named groups like (?&lt;a&gt;...) and (?&lt;b&gt;...).\n' +
'Escape special characters such as [](). when they should be matched literally;\n' +
'it is recommended to use ^ and $ to constrain the full filename, especially $, to avoid false positives from partial matches.',
episodeFormatRuleAdd: 'Add Rule',
episodeFormatRuleEdit: 'Edit Rule',
episodeFormatRuleDeleteConfirm: 'Are you sure you want to delete this rule?',
episodeFormatRuleSaveSuccess: 'Episode format rules saved successfully',
episodeFormatRuleSaveFailed: 'Failed to save episode format rules!',
episodeFormatRuleEmptyError: 'Rule name and regular expression cannot be empty',
},
search: {
basicSettings: 'Basic Settings',
@@ -2522,6 +2546,16 @@ export default {
episodeFormat: 'Episode Positioning',
episodeFormatHint: 'Use {ep} to position episode number part in filename to assist recognition',
episodeFormatPlaceholder: 'Use {ep} to position episode',
episodeFormatRecommendAction: 'Generate',
episodeFormatRecommendLoading: 'Generating...',
episodeFormatRecommendSelectFile: 'Please select a single file or directory first',
episodeFormatRecommendNeedWords:
'Manual episode positioning rules are empty, please fill them in on the words page first',
episodeFormatRecommendSuccess: 'Episode format template generated',
episodeFormatRecommendOverwriteSuccess: 'Current episode format has been overwritten by the generated result',
episodeFormatRecommendFailed: 'Failed to generate episode format, please try again later',
episodeFormatRecommendRule: 'Matched Rule: {rule}',
episodeFormatRecommendSample: 'Sample File: {file}',
episodeOffset: 'Episode Offset',
episodeOffsetHint: 'Episode offset calculation, e.g. -10 or EP*2',
episodeOffsetPlaceholder: 'e.g. -10',

View File

@@ -1834,6 +1834,29 @@ export default {
excludeWordsHint: '支持正则表达式,特殊字符需要\\转义,一行代表一个屏蔽词',
excludeWordsSaveSuccess: '文件整理屏蔽词保存成功',
excludeWordsSaveFailed: '文件整理屏蔽词保存失败!',
episodeFormatRule: '手动整理集数定位规则',
episodeFormatRuleDesc: '用于匹配媒体文件名,命中后自动生成对应的集数定位正则。',
episodeFormatRuleName: '规则名称',
episodeFormatRuleNameHint: '输入规则的名称',
episodeFormatRulePattern: '正则表达式',
episodeFormatRulePatternHint: '必须包含 (?&lt;ep&gt;...) 命名组',
episodeFormatRuleMinSize: '最小大小MB',
episodeFormatRuleMinSizeHint: '小于此大小的文件将不参与匹配',
episodeFormatRuleGuideTitle: '提示',
episodeFormatRuleGuideContent:
'从上到下依次匹配,第一个匹配的则使用该规则。\n' +
'按文件名实际结构编写匹配规则:\n' +
'固定文本直接匹配,集数必须使用命名分组 (?&lt;ep&gt;...) 标记;\n' +
'其余不固定但需要保留位置的内容,可使用 (?&lt;a&gt;...)、(?&lt;b&gt;...) 等命名分组承接。\n' +
'如需按字面量匹配 [](). 等特殊字符,请进行转义;\n' +
'建议使用 ^ 和 $ 完整约束整条文件名,其中 $ 尤其重要,可避免只匹配前半段就被误判为命中。',
episodeFormatRuleAdd: '新增规则',
episodeFormatRuleEdit: '编辑规则',
episodeFormatRuleDeleteConfirm: '确定要删除该规则吗?',
episodeFormatRuleEmptyError: '名称和正则表达式不能为空',
episodeFormatRuleSaveSuccess: '集数定位规则保存成功',
episodeFormatRuleSaveFailed: '集数定位规则保存失败!',
},
search: {
basicSettings: '基础设置',
@@ -2477,6 +2500,15 @@ export default {
episodeFormat: '集数定位',
episodeFormatHint: '使用{ep}定位文件名中的集数部分以辅助识别',
episodeFormatPlaceholder: '使用{ep}定位集数',
episodeFormatRecommendAction: '智能生成',
episodeFormatRecommendLoading: '生成中...',
episodeFormatRecommendSelectFile: '请先选择单个文件或目录',
episodeFormatRecommendNeedWords: '手动整理集数定位规则为空,请先前往词表填写',
episodeFormatRecommendSuccess: '已生成集数定位模板',
episodeFormatRecommendOverwriteSuccess: '已用智能生成结果覆盖当前集数定位',
episodeFormatRecommendFailed: '集数定位生成失败,请稍后重试',
episodeFormatRecommendRule: '命中规则:{rule}',
episodeFormatRecommendSample: '样本文件:{file}',
episodeOffset: '集数偏移',
episodeOffsetHint: '集数偏移运算,如-10或EP*2',
episodeOffsetPlaceholder: '如-10',

View File

@@ -1835,6 +1835,29 @@ export default {
excludeWordsHint: '支持正則表達式,特殊字符需要\\轉義,一行代表一個屏蔽詞',
excludeWordsSaveSuccess: '文件整理屏蔽詞保存成功',
excludeWordsSaveFailed: '文件整理屏蔽詞保存失敗!',
episodeFormatRule: '手動整理集數定位規則',
episodeFormatRuleDesc: '通過正則表達式提取文件名中的集數。',
episodeFormatRuleName: '規則名稱',
episodeFormatRuleNameHint: '輸入規則的名稱',
episodeFormatRulePattern: '正則表達式',
episodeFormatRulePatternHint: '必須包含 (?&lt;ep&gt;...) 命名組',
episodeFormatRuleMinSize: '最小大小MB',
episodeFormatRuleMinSizeHint: '小於此大小的文件將不參與匹配',
episodeFormatRuleGuideTitle: '提示',
episodeFormatRuleGuideContent:
'從上到下依次匹配,第一個匹配的則使用該規則。\n' +
'按文件名實際結構編寫匹配規則:\n' +
'固定文本直接匹配,集數必須使用命名分組 (?&lt;ep&gt;...) 標記;\n' +
'其餘不固定但需要保留位置的內容,可使用 (?&lt;a&gt;...)、(?&lt;b&gt;...) 等命名分組承接。\n' +
'如需按字面量匹配 [](). 等特殊字符,請進行轉義;\n' +
'建議使用 ^ 和 $ 完整約束整條文件名,其中 $ 尤其重要,可避免只匹配前半段就被誤判為命中。',
episodeFormatRuleAdd: '新增規則',
episodeFormatRuleEdit: '編輯規則',
episodeFormatRuleDeleteConfirm: '確定要刪除該規則嗎?',
episodeFormatRuleSaveSuccess: '集數定位規則保存成功',
episodeFormatRuleSaveFailed: '集數定位規則保存失敗!',
episodeFormatRuleEmptyError: '規則名稱和正則表達式不能為空',
},
search: {
basicSettings: '基礎設置',
@@ -2478,6 +2501,15 @@ export default {
episodeFormat: '集數定位',
episodeFormatHint: '使用{ep}定位文件名中的集數部分以輔助識別',
episodeFormatPlaceholder: '使用{ep}定位集數',
episodeFormatRecommendAction: '智能生成',
episodeFormatRecommendLoading: '生成中...',
episodeFormatRecommendSelectFile: '請先選擇單個文件或目錄',
episodeFormatRecommendNeedWords: '手動整理集數定位規則為空,請先前往詞表填寫',
episodeFormatRecommendSuccess: '已生成集數定位模板',
episodeFormatRecommendOverwriteSuccess: '已用智能生成結果覆蓋當前集數定位',
episodeFormatRecommendFailed: '集數定位生成失敗,請稍後重試',
episodeFormatRecommendRule: '命中規則:{rule}',
episodeFormatRecommendSample: '樣本文件:{file}',
episodeOffset: '集數偏移',
episodeOffsetHint: '集數偏移運算,如-10或EP*2',
episodeOffsetPlaceholder: '如-10',

View File

@@ -3,12 +3,62 @@ import { useToast } from 'vue-toastification'
import api from '@/api'
import { useI18n } from 'vue-i18n'
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
// 国际化
const { t } = useI18n()
// 提示框
const $toast = useToast()
// 集数定位规则
interface EpisodeFormatRule {
_localId: string
name: string
enabled: boolean
order: number
pattern: string
min_file_size_mb: number
}
const episodeFormatRules = ref<EpisodeFormatRule[]>([])
function createEpisodeRuleLocalId() {
return `episode-rule-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
function createEpisodeRule(rule?: Partial<Omit<EpisodeFormatRule, '_localId'>>): EpisodeFormatRule {
return {
_localId: createEpisodeRuleLocalId(),
name: rule?.name ?? '',
enabled: rule?.enabled ?? true,
order: rule?.order ?? episodeFormatRules.value.length + 1,
pattern: rule?.pattern ?? '',
min_file_size_mb: rule?.min_file_size_mb ?? 500,
}
}
function normalizeEpisodeFormatRules(
rules: Array<Partial<Omit<EpisodeFormatRule, '_localId'>> & { _localId?: string }> = [],
) {
return rules.map(rule => createEpisodeRule(rule))
}
function buildEpisodeFormatRulePayload() {
return episodeFormatRules.value.map((rule, index) => ({
name: rule.name,
enabled: rule.enabled,
order: index + 1,
pattern: rule.pattern,
min_file_size_mb: Number(rule.min_file_size_mb) || 0,
}))
}
// 添加集数定位规则
function addEpisodeRule() {
episodeFormatRules.value.push(createEpisodeRule())
}
// 自定义识别词
const customIdentifiers = ref('')
@@ -121,11 +171,67 @@ async function saveTransferExcludeWords() {
}
}
// 查询集数定位规则
async function queryEpisodeFormatRules() {
try {
const result: { [key: string]: any } = await api.get('system/setting/EpisodeFormatRuleTable')
if (result && result.data && result.data.value) {
episodeFormatRules.value = normalizeEpisodeFormatRules(result.data.value)
} else {
episodeFormatRules.value = []
}
} catch (error) {
console.log(error)
}
}
// 保存集数定位规则
async function saveEpisodeFormatRules() {
// 基础校验
for (const rule of episodeFormatRules.value) {
if (!rule.name || !rule.pattern) {
$toast.error(t('setting.words.episodeFormatRuleEmptyError'))
return
}
}
try {
const payload = buildEpisodeFormatRulePayload()
episodeFormatRules.value.forEach((rule, index) => {
rule.order = payload[index].order
rule.min_file_size_mb = payload[index].min_file_size_mb
})
const result: { [key: string]: any } = await api.post('system/setting/EpisodeFormatRuleTable', payload)
if (result.success) {
$toast.success(t('setting.words.episodeFormatRuleSaveSuccess'))
queryEpisodeFormatRules()
} else {
$toast.error(result.message || t('setting.words.episodeFormatRuleSaveFailed'))
}
} catch (error) {
console.log(error)
$toast.error(t('setting.words.episodeFormatRuleSaveFailed'))
}
}
// 删除集数定位规则
function deleteEpisodeRule(index: number) {
if (confirm(t('setting.words.episodeFormatRuleDeleteConfirm'))) {
episodeFormatRules.value.splice(index, 1)
}
}
// 拖拽结束
function onEpisodeRuleDragEnd() {
saveEpisodeFormatRules()
}
onMounted(() => {
queryCustomIdentifiers()
queryCustomReleaseGroups()
queryCustomization()
queryTransferExcludeWords()
queryEpisodeFormatRules()
})
</script>
@@ -247,4 +353,165 @@ onMounted(() => {
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<template #append>
<VBtn color="primary" @click="addEpisodeRule" prepend-icon="mdi-plus">
{{ t('setting.words.episodeFormatRuleAdd') }}
</VBtn>
</template>
<VCardTitle>{{ t('setting.words.episodeFormatRule') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.words.episodeFormatRuleDesc') }}</VCardSubtitle>
</VCardItem>
<VCardText>
<Draggable
v-model="episodeFormatRules"
handle=".cursor-move"
item-key="_localId"
tag="div"
:component-data="{ class: 'd-flex flex-column gap-3' }"
@end="onEpisodeRuleDragEnd"
>
<template #item="{ element, index }">
<VCard variant="outlined" class="episode-rule-card">
<VCardText class="py-4">
<div class="episode-rule-row d-flex align-center gap-2">
<IconBtn
icon="mdi-drag"
variant="text"
size="small"
class="episode-rule-control episode-rule-drag cursor-move flex-0-0"
/>
<VCheckbox
v-model="element.enabled"
color="primary"
density="compact"
hide-details
class="episode-rule-control episode-rule-enabled flex-0-0"
/>
<div class="episode-rule-field episode-rule-name">
<VTextField
v-model="element.name"
:label="t('setting.words.episodeFormatRuleName')"
hide-details="auto"
density="comfortable"
required
/>
</div>
<div class="episode-rule-field episode-rule-pattern">
<VTextField
v-model="element.pattern"
:label="t('setting.words.episodeFormatRulePattern')"
hide-details="auto"
density="comfortable"
required
/>
</div>
<div class="episode-rule-field episode-rule-size">
<VTextField
v-model.number="element.min_file_size_mb"
:label="t('setting.words.episodeFormatRuleMinSize')"
type="number"
min="0"
hide-details="auto"
density="comfortable"
required
/>
</div>
<IconBtn
variant="text"
size="small"
color="error"
class="episode-rule-control episode-rule-delete flex-0-0"
@click="deleteEpisodeRule(index)"
>
<VIcon icon="mdi-delete" />
</IconBtn>
</div>
</VCardText>
</VCard>
</template>
</Draggable>
</VCardText>
<VCardText>
<VAlert type="info" variant="tonal" :title="t('setting.words.episodeFormatRuleGuideTitle')">
<div style="white-space: pre-line" v-html="t('setting.words.episodeFormatRuleGuideContent')"></div>
</VAlert>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveEpisodeFormatRules" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>
<style scoped>
.episode-rule-card {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
.episode-rule-row {
flex-wrap: nowrap;
}
.episode-rule-name {
flex: 0.8 1 9rem;
min-inline-size: 7rem;
}
.episode-rule-pattern {
flex: 3.7 1 26rem;
min-inline-size: 0;
}
.episode-rule-size {
flex: 0 0 8rem;
min-inline-size: 8rem;
}
@media (width <= 959px) {
.episode-rule-row {
flex-wrap: wrap;
align-items: flex-start !important;
}
.episode-rule-drag {
order: 1;
}
.episode-rule-enabled {
order: 2;
}
.episode-rule-delete {
order: 3;
margin-inline-start: auto;
}
.episode-rule-name {
flex: 1 1 calc(50% - 0.25rem);
order: 4;
min-inline-size: 0;
}
.episode-rule-size {
flex: 1 1 calc(50% - 0.25rem);
order: 5;
min-inline-size: 0;
}
.episode-rule-pattern {
flex: 1 1 100%;
order: 6;
}
}
</style>