Merge pull request #13 from Rewrite0/new-ui

New UI
This commit is contained in:
Rewrite0
2023-05-31 23:29:42 +08:00
committed by GitHub
105 changed files with 9986 additions and 2045 deletions

View File

@@ -1,7 +1,8 @@
{
"extends": ["@antfu", "prettier"],
"extends": ["@antfu", "prettier", "plugin:storybook/recommended"],
"rules": {
"antfu/if-newline": ["off"],
"no-console": ["off"]
"no-console": ["off"],
"vue/custom-event-name-casing": ["off"]
}
}

View File

@@ -3,3 +3,4 @@
/pnpm-lock.yaml
auto-imports.d.ts
components.d.ts
router-type.d.ts

24
.storybook/main.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { StorybookConfig } from '@storybook/vue3-vite';
import Unocss from 'unocss/vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal(config) {
config.plugins?.push(Unocss());
// Add other configuration here depending on your use case
return config;
},
};
export default config;

17
.storybook/preview.ts Normal file
View File

@@ -0,0 +1,17 @@
import type { Preview } from '@storybook/vue3';
import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@@ -1,15 +1,3 @@
# Auto_Bangumi_WebUI
基于[Auto_Bangumi](https://github.com/EstrellaXD/Auto_Bangumi)的 WebUI
目前适配了`Auto_Bangumi`的 v1 版本 api
主要功能为:
- 查看订阅番剧(仅 mikan 源)
- 订阅其他来源的新番 rss
- 订阅旧番 rss
- debug
- 重置`Auto_Bangumi`的数据
- 查看日志
- config 设置页
使用 Vue3 + TypeScript 构建的 [Auto_Bangumi](https://github.com/EstrellaXD/Auto_Bangumi) 的 WebUI

View File

@@ -4,40 +4,57 @@
"version": "0.0.0",
"private": true,
"scripts": {
"build": "pnpm lint && vite build",
"build": "vue-tsc --noEmit && vite build",
"dev": "vite",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"preview": "vite preview",
"test": "vitest"
"test": "vitest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@headlessui/vue": "^1.7.13",
"@vueuse/core": "^8.9.4",
"axios": "^0.27.2",
"element-plus": "^2.3.4",
"modern-normalize": "^1.1.0",
"pinia": "^2.0.35",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"lodash": "^4.17.21",
"naive-ui": "^2.34.4",
"pinia": "^2.1.3",
"vue": "^3.3.4",
"vue-router": "^4.2.1"
},
"devDependencies": {
"@antfu/eslint-config": "^0.38.5",
"@types/node": "^18.16.0",
"@unocss/preset-rem-to-px": "^0.51.8",
"@unocss/reset": "^0.51.8",
"@vitejs/plugin-vue": "^3.2.0",
"eslint": "^8.39.0",
"@antfu/eslint-config": "^0.38.6",
"@icon-park/vue-next": "^1.4.2",
"@storybook/addon-essentials": "^7.0.12",
"@storybook/addon-interactions": "^7.0.12",
"@storybook/addon-links": "^7.0.12",
"@storybook/blocks": "^7.0.12",
"@storybook/testing-library": "0.0.14-next.2",
"@storybook/vue3": "^7.0.12",
"@storybook/vue3-vite": "^7.0.12",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.14",
"@unocss/preset-rem-to-px": "^0.51.13",
"@unocss/reset": "^0.51.13",
"@vitejs/plugin-vue": "^4.2.0",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-storybook": "^0.6.12",
"prettier": "^2.8.8",
"sass": "^1.62.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.62.1",
"storybook": "^7.0.12",
"typescript": "^4.9.5",
"unocss": "^0.51.8",
"unocss": "^0.51.13",
"unplugin-auto-import": "^0.10.3",
"unplugin-vue-components": "^0.21.2",
"vite": "^3.2.6",
"unplugin-vue-components": "^0.24.1",
"unplugin-vue-router": "^0.6.4",
"vite": "^4.3.5",
"vitest": "^0.30.1",
"vue-tsc": "^0.38.9"
"vue-tsc": "^1.6.4"
}
}

7002
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,31 @@
<script setup lang="ts">
import zhCn from 'element-plus/lib/locale/lang/zh-cn';
import {
type GlobalThemeOverrides,
NConfigProvider,
NMessageProvider,
} from 'naive-ui';
const { getStatus, onUpdate } = programStore();
getStatus();
onUpdate();
const theme: GlobalThemeOverrides = {
Spin: {
color: '#fff',
},
};
const { refresh } = useAuth();
refresh();
</script>
<template>
<Suspense>
<el-config-provider :locale="zhCn">
<RouterView />
</el-config-provider>
<NConfigProvider :theme-overrides="theme">
<NMessageProvider>
<RouterView></RouterView>
</NMessageProvider>
</NConfigProvider>
</Suspense>
</template>
<style lang="scss">
@import './style/transition';
@import './style/global';
</style>

41
src/api/auth.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { LoginSuccess, Logout, Update } from '#/auth';
export const apiAuth = {
async login(username: string, password: string) {
const formData = new URLSearchParams({
username,
password,
});
const { data } = await axios.post<LoginSuccess>(
'api/v1/auth/login',
formData,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return data;
},
async refresh() {
const { data } = await axios.get<LoginSuccess>('api/v1/auth/refresh_token');
return data;
},
async logout() {
const { data } = await axios.get<Logout>('api/v1/auth/logout');
return data.message === 'logout success';
},
async update(username: string, password: string) {
const { data } = await axios.post<Update>('api/v1/auth/update', {
username,
password,
});
return data;
},
};

View File

@@ -1,25 +1,91 @@
import axios from 'axios';
import type { BangumiItem } from '#/bangumi';
import type { BangumiRule } from '#/bangumi';
/**
* 添加番剧订阅
* @param type 'new' 添加新番, old 添加旧番
* @param rss_link
*/
function addBangumi(type: string, rss_link: string) {
if (type === 'new') {
return axios.post('api/v1/subscribe', { rss_link });
} else if (type === 'old') {
return axios.post('api/v1/collection', { rss_link });
} else {
console.error('type错误, type应为 new 或 old');
return false;
}
}
export const apiBangumi = {
/**
* 获取所有 bangumi 数据
* @returns 所有 bangumi 数据
*/
async getAll() {
const { data } = await axios.get<BangumiRule[]>('api/v1/bangumi/getAll');
/**
* 获取订阅番剧数据
*/
const getABData = () => axios.get<BangumiItem[]>('api/v1/bangumi/getAll');
return data;
},
export { addBangumi, getABData };
/**
* 获取指定 bangumiId 的规则
* @param bangumiId bangumi id
* @returns 指定 bangumi 的规则
*/
async getRule(bangumiId: number) {
const { data } = await axios.get<BangumiRule>(
`api/v1/bangumi/getRule/${bangumiId}`
);
return data;
},
/**
* 更新指定 bangumiId 的规则
* @param bangumiData - 需要更新的规则
* @returns axios 请求返回的数据
*/
async updateRule(bangumiRule: BangumiRule) {
const { data } = await axios.post<{
msg: string;
status: 'success';
}>('api/v1/bangumi/updateRule', bangumiRule);
return data;
},
/**
* 删除指定 bangumiId 的数据库规则,会在重新匹配到后重建
* @param bangumiId - 需要删除的 bangumi 的 id
* @returns axios 请求返回的数据
*/
async deleteRule(bangumiId: number) {
const { data } = await axios.delete(
`api/v1/bangumi/deleteRule/${bangumiId}`
);
return data;
},
/**
* 删除指定 bangumiId 的规则。如果 file 为 true则同时删除关联文件。
* @param bangumiId - 需要删除规则的 bangumi 的 id。
* @param file - 是否同时删除关联文件。
* @returns axios 请求返回的数据
*/
async disableRule(bangumiId: number, file: boolean) {
const { data } = await axios.delete<{
status: 'success';
msg: string;
}>(`api/v1/bangumi/disableRule/${bangumiId}`, {
params: {
file,
},
});
return data;
},
/**
* 启用指定 bangumiId 的规则
* @param bangumiId - 需要启用的 bangumi 的 id
*/
async enableRule(bangumiId: number) {
const { data } = await axios.get<{
status: 'success';
msg: string;
}>(`api/v1/bangumi/enableRule/${bangumiId}`);
return data;
},
/**
* 重置所有 bangumi 数据
*/
async resetAll() {
const { data } = await axios.post<{
status: 'ok';
}>('api/v1/bangumi/resetAll');
return data;
},
};

25
src/api/check.ts Normal file
View File

@@ -0,0 +1,25 @@
export const apiCheck = {
/**
* 检测下载器
*/
async downloader() {
const { data } = await axios.get('api/v1/check/downloader');
return data;
},
/**
* 检测 RSS
*/
async rss() {
const { data } = await axios.get('api/v1/check/rss');
return data;
},
/**
* 检测所有
*/
async all() {
const { data } = await axios.get('api/v1/check');
return data;
},
};

View File

@@ -1,15 +1,23 @@
import axios from 'axios';
import type { Config } from '#/config';
export async function setConfig(newConfig: Config) {
const { data } = await axios.post<{
message: 'Success' | 'Failed to update config';
}>('api/v1/updateConfig', newConfig);
export const apiConfig = {
/**
* 获取 config 数据
*/
async getConfig() {
const { data } = await axios.get<Config>('api/v1/getConfig');
return data;
},
return data.message === 'Success';
}
/**
* 更新 config 数据
* @param newConfig - 需要更新的 config
*/
async updateConfig(newConfig: Config) {
const { data } = await axios.post<{
message: 'Success' | 'Failed to update config';
}>('api/v1/updateConfig', newConfig);
export async function getConfig() {
const { data } = await axios.get<Config>('api/v1/getConfig');
return data;
}
return data.message === 'Success';
},
};

View File

@@ -1,15 +0,0 @@
import axios from 'axios';
/**
* 获取AB的日志
*/
async function getABLog() {
const { data } = await axios.get('api/v1/log');
return data;
}
/**
* 重置 AB 的数据,程序会在下一轮检索中重新添加 RSS 订阅信息。
*/
const resetRule = () => axios.get('api/v1/resetRule');
export { getABLog, resetRule };

61
src/api/download.ts Normal file
View File

@@ -0,0 +1,61 @@
import type { BangumiRule } from '#/bangumi';
interface Status {
status: 'Success';
}
interface AnalysisError {
status: 'Failed to parse link';
}
export const apiDownload = {
/**
* 解析 RSS 链接
* @param rss_link - RSS 链接
*/
async analysis(rss_link: string) {
const fetchResult = createEventHook<BangumiRule>();
const fetchError = createEventHook<AnalysisError>();
axios
.post<any>('api/v1/download/analysis', {
rss_link,
})
.then(({ data }) => {
if (data.status) {
fetchError.trigger(data as AnalysisError);
} else {
fetchResult.trigger(data as BangumiRule);
}
});
return {
onResult: fetchResult.on,
onError: fetchError.on,
};
},
/**
* 旧番
* @param bangumiData - Bangumi 数据
*/
async collection(bangumiData: BangumiRule) {
const { data } = await axios.post<Status>(
'api/v1/download/collection',
bangumiData
);
return data.status === 'Success';
},
/**
* 新番
* @param bangumiData - Bangumi 数据
*/
async subscribe(bangumiData: BangumiRule) {
const { data } = await axios.post<Status>(
'api/v1/download/subscribe',
bangumiData
);
return data.status === 'Success';
},
};

11
src/api/log.ts Normal file
View File

@@ -0,0 +1,11 @@
export const apiLog = {
async getLog() {
const { data } = await axios.get<string>('api/v1/log');
return data;
},
async clearLog() {
const { data } = await axios.get<{ status: 'ok' }>('api/v1/log/clear');
return data.status === 'ok';
},
};

View File

@@ -1,21 +1,47 @@
import axios from 'axios';
/** 重启 */
export async function appRestart() {
const { data } = await axios.get<{ status: 'ok' }>('api/v1/restart');
return data.status === 'ok';
interface Success {
status: 'ok';
}
/** 启动 */
export const appStart = () => axios.get('api/v1/start');
export const apiProgram = {
/**
* 重启
*/
async restart() {
const { data } = await axios.get<Success>('api/v1/restart');
return data.status === 'ok';
},
/** 停止 */
export const appStop = () => axios.get('api/v1/stop');
/**
* 启动
*/
async start() {
const { data } = await axios.get<Success>('api/v1/start');
return data.status === 'ok';
},
/** 状态 */
export async function appStatus() {
const { data } = await axios.get<{ status: 'stop' | 'running' }>(
'api/v1/status'
);
return data.status !== 'stop';
}
/**
* 停止
*/
async stop() {
const { data } = await axios.get<Success>('api/v1/stop');
return data.status === 'ok';
},
/**
* 状态
*/
async status() {
const { data } = await axios.get<{ status: 'running' | 'stop' }>(
'api/v1/status'
);
return data.status === 'running';
},
/**
* 终止
*/
async shutdown() {
const { data } = await axios.get<Success>('api/v1/shutdown');
return data.status === 'ok';
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

221
src/auto-imports.d.ts vendored
View File

@@ -5,33 +5,64 @@ declare global {
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const afterAll: typeof import('vitest')['afterAll']
const afterEach: typeof import('vitest')['afterEach']
const apiAuth: typeof import('./api/auth')['apiAuth']
const apiBangumi: typeof import('./api/bangumi')['apiBangumi']
const apiCheck: typeof import('./api/check')['apiCheck']
const apiConfig: typeof import('./api/config')['apiConfig']
const apiDownload: typeof import('./api/download')['apiDownload']
const apiLog: typeof import('./api/log')['apiLog']
const apiProgram: typeof import('./api/program')['apiProgram']
const assert: typeof import('vitest')['assert']
const bangumiStore: typeof import('./store/bangumi')['bangumiStore']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const axios: typeof import('./utils/axios')['axios']
const beforeAll: typeof import('vitest')['beforeAll']
const beforeEach: typeof import('vitest')['beforeEach']
const chai: typeof import('vitest')['chai']
const computed: typeof import('vue')['computed']
const configStore: typeof import('./store/config')['configStore']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineLoader: typeof import('vue-router/auto')['defineLoader']
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
const defineStore: typeof import('pinia')['defineStore']
const describe: typeof import('vitest')['describe']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const expect: typeof import('vitest')['expect']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const it: typeof import('vitest')['it']
const logStore: typeof import('./store/log')['logStore']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
@@ -41,23 +72,41 @@ declare global {
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const programStore: typeof import('./store/program')['programStore']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
@@ -65,22 +114,182 @@ declare global {
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const suite: typeof import('vitest')['suite']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const test: typeof import('vitest')['test']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useApi: typeof import('./hooks/useApi')['useApi']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAuth: typeof import('./hooks/useAuth')['useAuth']
const useBangumiStore: typeof import('./store/bangumi')['useBangumiStore']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfigStore: typeof import('./store/config')['useConfigStore']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStore: typeof import('./store/log')['useLogStore']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMessage: typeof import('./hooks/useMessage')['useMessage']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePlayerStore: typeof import('./store/player')['usePlayerStore']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useProgramStore: typeof import('./store/program')['useProgramStore']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router/auto')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const vi: typeof import('vitest')['vi']
const vitest: typeof import('vitest')['vitest']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}

59
src/components.d.ts vendored
View File

@@ -1,5 +1,7 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
@@ -7,32 +9,35 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ConfigFormCol: typeof import('./components/ConfigFormCol.vue')['default']
ConfigFormRow: typeof import('./components/ConfigFormRow.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
AbAdd: typeof import('./components/basic/ab-add.vue')['default']
AbAddBangumi: typeof import('./components/ab-add-bangumi.vue')['default']
AbBangumiCard: typeof import('./components/ab-bangumi-card.vue')['default']
AbButton: typeof import('./components/basic/ab-button.vue')['default']
AbChangeAccount: typeof import('./components/ab-change-account.vue')['default']
AbCheckbox: typeof import('./components/basic/ab-checkbox.vue')['default']
AbContainer: typeof import('./components/ab-container.vue')['default']
AbEditRule: typeof import('./components/ab-edit-rule.vue')['default']
AbFoldPanel: typeof import('./components/ab-fold-panel.vue')['default']
AbLabel: typeof import('./components/ab-label.vue')['default']
AbPageTitle: typeof import('./components/basic/ab-page-title.vue')['default']
AbPopup: typeof import('./components/ab-popup.vue')['default']
AbRule: typeof import('./components/ab-rule.vue')['default']
AbSearch: typeof import('./components/basic/ab-search.vue')['default']
AbSelect: typeof import('./components/basic/ab-select.vue')['default']
AbSetting: typeof import('./components/ab-setting.vue')['default']
AbSidebar: typeof import('./components/layout/ab-sidebar.vue')['default']
AbStatus: typeof import('./components/basic/ab-status.vue')['default']
AbStatusBar: typeof import('./components/ab-status-bar.vue')['default']
AbSwitch: typeof import('./components/basic/ab-switch.vue')['default']
AbTopbar: typeof import('./components/layout/ab-topbar.vue')['default']
ConfigDownload: typeof import('./components/setting/config-download.vue')['default']
ConfigManage: typeof import('./components/setting/config-manage.vue')['default']
ConfigNormal: typeof import('./components/setting/config-normal.vue')['default']
ConfigNotification: typeof import('./components/setting/config-notification.vue')['default']
ConfigParser: typeof import('./components/setting/config-parser.vue')['default']
ConfigPlayer: typeof import('./components/setting/config-player.vue')['default']
ConfigProxy: typeof import('./components/setting/config-proxy.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ShowResults: typeof import('./components/ShowResults.vue')['default']
}
}

View File

@@ -1,14 +0,0 @@
<script lang="ts" setup>
defineProps<{
label: string;
prop?: string | string[];
}>();
</script>
<template>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
<el-form-item :label="label" :prop="prop ?? ''">
<slot></slot>
</el-form-item>
</el-col>
</template>

View File

@@ -1,17 +0,0 @@
<script lang="ts" setup>
defineProps<{
title: string;
}>();
</script>
<template>
<el-collapse-item>
<template #title>
<span font-bold text-base>{{ title }}</span>
</template>
<el-row :gutter="20">
<slot></slot>
</el-row>
</el-collapse-item>
</template>

View File

@@ -1,46 +0,0 @@
<script setup lang="ts">
defineProps<{
title: string;
results: object | null;
}>();
const dialogVisible = ref(false);
function handleClose() {
dialogVisible.value = false;
}
function open() {
dialogVisible.value = true;
}
defineExpose({
open,
});
</script>
<template>
<div class="dia-log">
<el-dialog
v-model="dialogVisible"
:title="title"
width="30%"
:before-close="handleClose"
>
<div>
<div class="results">
{{ results === null ? 'null' : results }}
</div>
</div>
</el-dialog>
</div>
</template>
<style lang="scss" scope>
.dia-log {
.results {
padding: 1em;
line-height: 1.5;
color: #fff;
background: #333;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import type { BangumiRule } from '#/bangumi';
const { getAll } = useBangumiStore();
const show = defineModel('show', { default: false });
const rss = ref('');
const message = useMessage();
const rule = ref<BangumiRule>({
added: false,
deleted: false,
dpi: '',
eps_collect: false,
filter: [],
group_name: '',
id: 0,
official_title: '',
offset: 0,
poster_link: '',
rss_link: [],
rule_name: '',
save_path: '',
season: 1,
season_raw: '',
source: null,
subtitle: '',
title_raw: '',
year: null,
});
const analysis = reactive({
loading: false,
next: false,
});
const loading = reactive({
collect: false,
subscribe: false,
});
watch(show, (val) => {
if (!val) {
rss.value = '';
setTimeout(() => {
analysis.next = false;
}, 300);
}
});
async function analyser() {
if (rss.value === '') {
message.error('Please enter the RSS link!');
} else {
try {
analysis.loading = true;
const { onError, onResult } = await apiDownload.analysis(rss.value);
onResult((data) => {
rule.value = data;
analysis.loading = false;
analysis.next = true;
console.log('rule', data);
});
onError((err) => {
message.error(err.status);
analysis.loading = false;
console.log('error', err);
});
} catch (error) {
message.error('Failed to analyser!');
}
}
}
async function collect() {
if (rule.value) {
try {
loading.collect = true;
const res = await apiDownload.collection(rule.value);
loading.collect = false;
if (res) {
message.success('Collect Success!');
getAll();
show.value = false;
} else {
message.error('Collect Failed!');
}
} catch (error) {
message.error('Collect Error!');
}
}
}
async function subscribe() {
if (rule.value) {
try {
loading.subscribe = true;
const res = await apiDownload.subscribe(rule.value);
loading.subscribe = false;
if (res) {
message.success('Subscribe Success!');
getAll();
show.value = false;
} else {
message.error('Subscribe Failed!');
}
} catch (error) {
message.error('Subscribe Error!');
}
}
}
</script>
<template>
<ab-popup v-model:show="show" title="Add Bangumi" css="w-360px">
<div v-if="!analysis.next" space-y-12px>
<ab-setting
v-model:data="rss"
label="RSS Link"
type="input"
:prop="{
placeholder: 'Please enter the RSS link',
}"
:bottom-line="true"
></ab-setting>
<div flex="~ justify-end">
<ab-button size="small" :loading="analysis.loading" @click="analyser"
>Analyse</ab-button
>
</div>
</div>
<div v-else>
<ab-rule v-model:rule="rule"></ab-rule>
<div flex="~ justify-end" space-x-10px>
<ab-button size="small" :loading="loading.collect" @click="collect"
>Collect</ab-button
>
<ab-button size="small" :loading="loading.subscribe" @click="subscribe"
>Subscribe</ab-button
>
</div>
</div>
</ab-popup>
</template>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { ErrorPicture, Write } from '@icon-park/vue-next';
withDefaults(
defineProps<{
poster: string;
name: string;
season: number;
}>(),
{}
);
defineEmits(['click']);
</script>
<template>
<div w-150px is-btn @click="() => $emit('click')">
<div rounded-4px overflow-hidden poster-shandow rel>
<div w-full h-210px>
<template v-if="poster !== ''">
<img :src="`https://mikanani.me${poster}`" alt="poster" wh-full />
</template>
<template v-else>
<div wh-full f-cer border="1 white">
<ErrorPicture theme="outline" size="24" fill="#333" />
</div>
</template>
</div>
<div
abs
f-cer
z-1
inset-0
opacity-0
transition-all
duration-300
hover:backdrop-blur-2px
hover:bg-white
hover:bg-opacity-30
hover:opacity-100
active:duration-0
active:bg-opacity-60
class="group"
>
<div
text-white
rounded="1/2"
wh-44px
f-cer
bg-theme-row
class="group-active:poster-pen-active"
>
<Write size="20" />
</div>
</div>
</div>
<div px-4px py-8px>
<div text-h3 truncate>{{ name }}</div>
<div text-main>Season {{ season }}</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,37 @@
<script lang="ts" setup>
const show = defineModel('show', {
default: false,
});
const { user, update } = useAuth();
</script>
<template>
<ab-popup v-model:show="show" title="Change Account" css="w-365px">
<div space-y-16px>
<ab-label label="Username">
<input
v-model="user.username"
type="text"
placeholder="username"
ab-input
/>
</ab-label>
<ab-label label="Password">
<input
v-model="user.password"
type="password"
placeholder="password"
ab-input
/>
</ab-label>
<div line></div>
<div flex="~ justify-end">
<ab-button size="small" @click="update">Update</ab-button>
</div>
</div>
</ab-popup>
</template>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
title: string;
}>(),
{
title: 'title',
}
);
</script>
<template>
<div rounded-10px overflow-hidden>
<div
bg-theme-row
w-full
text-white
fx-cer
px-20px
h-45px
justify-between
select-none
>
<div text-h2>{{ title }}</div>
<slot name="title-right"></slot>
</div>
<div p-20px bg-white>
<slot></slot>
</div>
</div>
</template>

View File

@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { BangumiRule } from '#/bangumi';
const emit = defineEmits<{
(e: 'disable', opts: { id: number; deleteFile: boolean }): void;
(e: 'apply', rule: BangumiRule): void;
(e: 'enable', id: number): void;
}>();
const show = defineModel('show', { default: false });
const rule = defineModel<BangumiRule>('rule', {
required: true,
});
const deleteRuleDialog = ref(false);
watch(show, (val) => {
if (!val) {
deleteRuleDialog.value = false;
}
});
const close = () => (show.value = false);
function emitDisable(deleteFile: boolean) {
emit('disable', {
id: rule.value.id,
deleteFile,
});
}
function emitApply() {
emit('apply', rule.value);
}
function emitEnable() {
emit('enable', rule.value.id);
}
const popupTitle = computed(() => {
if (rule.value.deleted) {
return 'Enable Rule';
} else {
return 'Edit Rule';
}
});
const boxSize = computed(() => {
if (rule.value.deleted) {
return 'w-300px';
} else {
return 'w-380px';
}
});
</script>
<template>
<ab-popup v-model:show="show" :title="popupTitle" :css="boxSize">
<div v-if="rule.deleted">
<div>Do you want to enable this rule?</div>
<div line my-8px></div>
<div fx-cer justify-center space-x-10px>
<ab-button size="small" type="warn" @click="() => emitEnable()"
>Yes</ab-button
>
<ab-button size="small" @click="() => close()">No</ab-button>
</div>
</div>
<div v-else space-y-12px>
<ab-rule v-model:rule="rule"></ab-rule>
<div fx-cer justify-end space-x-10px>
<ab-button
size="small"
type="warn"
@click="() => (deleteRuleDialog = true)"
>Disable</ab-button
>
<ab-button size="small" @click="emitApply">Apply</ab-button>
</div>
</div>
<ab-popup v-model:show="deleteRuleDialog" title="Delete">
<div>Delete Local File?</div>
<div line my-8px></div>
<div fx-cer justify-center space-x-10px>
<ab-button size="small" type="warn" @click="() => emitDisable(true)"
>Yes</ab-button
>
<ab-button size="small" @click="() => emitDisable(false)">No</ab-button>
</div>
</ab-popup>
</ab-popup>
</template>

View File

@@ -0,0 +1,41 @@
<script lang="ts" setup>
import { Down, Up } from '@icon-park/vue-next';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
withDefaults(
defineProps<{
title: string;
}>(),
{
title: 'title',
}
);
</script>
<template>
<Disclosure v-slot="{ open }">
<div rounded-10px overflow-hidden h-max>
<DisclosureButton
bg-theme-row
w-full
text-white
fx-cer
px-20px
h-45px
justify-between
>
<div text-h2>{{ title }}</div>
<Component :is="open ? Up : Down" size="24" />
</DisclosureButton>
<div bg-white py-20px :class="[open ? 'px-20px' : 'px-8px']">
<div v-show="!open" line my-12px></div>
<DisclosurePanel>
<slot></slot>
</DisclosurePanel>
</div>
</div>
</Disclosure>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
label: string;
}>(),
{
label: '',
}
);
</script>
<template>
<div flex="~ items-start" justify-between>
<div>{{ label }}</div>
<slot> </slot>
</div>
</template>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
import {
Dialog,
DialogPanel,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue';
const props = withDefaults(
defineProps<{
title: string;
maskClick?: boolean;
css?: string;
}>(),
{
title: 'title',
maskClick: true,
css: '',
}
);
const show = defineModel('show', { default: false });
function close() {
if (props.maskClick) {
show.value = false;
}
}
</script>
<template>
<TransitionRoot appear :show="show" as="template">
<Dialog as="div" class="relative z-10" @close="close">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div
class="flex min-h-full items-center justify-center p-4 text-center"
>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel>
<ab-container :title="title" :class="[css]">
<slot></slot>
</ab-container>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import type { BangumiRule } from '#/bangumi';
import type { SettingItem } from '#/components';
const rule = defineModel<BangumiRule>('rule', {
required: true,
});
const items: SettingItem<BangumiRule>[] = [
{
configKey: 'official_title',
label: 'Officical Ttile',
type: 'input',
prop: {
type: 'text',
},
},
{
configKey: 'year',
label: 'Year',
type: 'input',
css: 'w-72px',
prop: {
type: 'text',
},
},
{
configKey: 'season',
label: 'Season',
type: 'input',
css: 'w-72px',
prop: {
type: 'number',
},
bottomLine: true,
},
{
configKey: 'offset',
label: 'Offset',
type: 'input',
css: 'w-72px',
prop: {
type: 'number',
},
},
{
configKey: 'filter',
label: 'Exclude',
type: 'dynamic-tags',
bottomLine: true,
},
];
</script>
<template>
<div space-y-12px>
<ab-setting
v-for="i in items"
:key="i.configKey"
v-bind="i"
v-model:data="rule[i.configKey]"
></ab-setting>
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { NDynamicTags } from 'naive-ui';
import type { AbSettingProps } from '#/components';
withDefaults(defineProps<AbSettingProps>(), {
css: '',
bottomLine: false,
});
const data = defineModel<any>('data');
</script>
<template>
<div>
<ab-label :label="label">
<AbSwitch
v-if="type === 'switch'"
v-model:checked="data"
v-bind="prop"
:class="css"
></AbSwitch>
<AbSelect
v-else-if="type === 'select'"
v-model="data"
v-bind="prop"
:class="css"
></AbSelect>
<input
v-else-if="type === 'input'"
v-model="data"
ab-input
:class="css"
v-bind="prop"
/>
<div v-else-if="type === 'dynamic-tags'" max-w-200px>
<NDynamicTags v-model:value="data" size="small"></NDynamicTags>
</div>
</ab-label>
<div v-if="bottomLine" line my-12px></div>
</div>
</template>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import { AddOne, More } from '@icon-park/vue-next';
withDefaults(
defineProps<{
running: boolean;
items: {
id: number;
icon: any;
label: string;
handle?: () => void | Promise<void>;
}[];
}>(),
{
running: false,
}
);
defineEmits(['clickAdd']);
</script>
<template>
<Menu>
<div rel>
<div fx-cer space-x-16px>
<AddOne
theme="outline"
size="24"
fill="#fff"
is-btn
btn-click
@click="() => $emit('clickAdd')"
/>
<MenuButton bg-transparent is-btn btn-click>
<More theme="outline" size="24" fill="#fff" />
</MenuButton>
<ab-status :running="running" />
</div>
<MenuItems
abs
top-50px
w-120px
rounded-8px
bg-white
overflow-hidden
shadow
z-99
>
<MenuItem v-for="i in items" :key="i.id" v-slot="{ active }">
<div
w-full
h-32px
px-12px
fx-cer
justify-between
is-btn
hover:text-white
hover:bg-primary
class="group"
:class="[active ? 'text-white bg-theme-row' : 'text-black']"
@click="i.handle"
>
<div text-main>{{ i.label }}</div>
<div
class="group-hover:text-white"
:class="[active ? 'text-white' : 'text-primary']"
>
<Component :is="i.icon" size="16"></Component>
</div>
</div>
</MenuItem>
</MenuItems>
</div>
</Menu>
</template>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbAdd from './ab-add.vue';
const meta: Meta<typeof AbAdd> = {
title: 'basic/ab-add',
component: AbAdd,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbAdd>;
export const Template: Story = {
render: (args) => ({
components: { AbAdd },
setup() {
return { args };
},
template: '<ab-add v-bind="args"></ab-add>',
}),
};

View File

@@ -0,0 +1,41 @@
<script lang="ts" setup>
defineEmits(['click']);
</script>
<template>
<button
rounded="1/2"
wh-36px
f-cer
rel
transition-colors
class="box"
@click="$emit('click')"
>
<div class="line" abs></div>
<div class="line" abs rotate-90></div>
</button>
</template>
<style lang="scss" scoped>
$normal: #493475;
$hover: #756596;
.box {
background: $normal;
&:hover {
background: $hover;
}
&:active {
background: $normal;
}
}
.line {
width: 6px;
height: 18px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbButton from './ab-button.vue';
const meta: Meta<typeof AbButton> = {
title: 'basic/ab-button',
component: AbButton,
tags: ['autodocs'],
argTypes: {
type: {
control: { type: 'select' },
options: ['primary', 'warn'],
},
size: {
control: { type: 'select' },
options: ['big', 'normal', 'small'],
},
},
};
export default meta;
type Story = StoryObj<typeof AbButton>;
export const Template: Story = {
render: (args) => ({
components: { AbButton },
setup() {
return { args };
},
template: '<ab-button v-bind="args">button</ab-button>',
}),
};

View File

@@ -0,0 +1,70 @@
<script lang="ts" setup>
import { NSpin } from 'naive-ui';
const props = withDefaults(
defineProps<{
type?: 'primary' | 'warn';
size?: 'big' | 'normal' | 'small';
link?: string | null;
loading?: boolean;
}>(),
{
type: 'primary',
size: 'normal',
link: null,
loading: false,
}
);
defineEmits(['click']);
const buttonSize = computed(() => {
switch (props.size) {
case 'big':
return 'rounded-10px text-h1 w-276px h-55px text-h1';
case 'normal':
return 'rounded-6px w-170px h-36px';
case 'small':
return 'rounded-6px w-86px h-28px text-main';
}
});
const loadingSize = computed(() => {
switch (props.size) {
case 'big':
return 'large';
case 'normal':
return 'small';
case 'small':
return 18;
}
});
</script>
<template>
<Component
:is="link !== null ? 'a' : 'button'"
:href="link"
text-white
outline-none
f-cer
:class="[`type-${type}`, buttonSize]"
@click="$emit('click')"
>
<NSpin :show="loading" :size="loadingSize">
<slot></slot>
</NSpin>
</Component>
</template>
<style lang="scss" scoped>
.type {
&-primary {
@include bg-mouse-event(#4e3c94, #281e52, #8e8a9c);
}
&-warn {
@include bg-mouse-event(#943c61, #521e2a, #9c8a93);
}
}
</style>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbCheckbox from './ab-checkbox.vue';
const meta: Meta<typeof AbCheckbox> = {
title: 'basic/ab-checkbox',
component: AbCheckbox,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbCheckbox>;
export const Template: Story = {
render: (args) => ({
components: { AbCheckbox },
setup() {
return { args };
},
template: '<ab-checkbox v-bind="args" />',
}),
};

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { Switch } from '@headlessui/vue';
withDefaults(
defineProps<{
small?: boolean;
}>(),
{
small: false,
}
);
const checked = defineModel<boolean>({ default: false });
</script>
<template>
<Switch v-model="checked" as="template">
<div flex items-center space-x-8px is-btn>
<slot name="before"></slot>
<div
rounded-4px
rel
f-cer
bg-white
border="3px #3c239f"
:class="[small ? 'wh-16px' : 'wh-32px', !checked && 'group']"
>
<div
rounded-2px
transition-all
duration-300
:class="[
small ? 'wh-8px' : 'wh-16px',
checked ? 'bg-[#3c239f]' : 'bg-transparent',
]"
group-hover:bg="#cccad4"
group-active:bg="#3c239f"
></div>
</div>
<slot name="after"></slot>
</div>
</Switch>
</template>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbPageTitle from './ab-page-title.vue';
const meta: Meta<typeof AbPageTitle> = {
title: 'basic/ab-PageTitle',
component: AbPageTitle,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbPageTitle>;
export const Template: Story = {
render: (args) => ({
components: { AbPageTitle },
setup() {
return { args };
},
template: '<ab-page-title v-bind="args" />',
}),
};

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
title: string;
}>(),
{
title: 'title',
}
);
</script>
<template>
<div fx-cer space-x-12px>
<div text-h1>{{ title }}</div>
<div w-160px h-3px bg-theme-row rounded-full></div>
</div>
</template>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbSearch from './ab-search.vue';
const meta: Meta<typeof AbSearch> = {
title: 'basic/ab-search',
component: AbSearch,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbSearch>;
export const Template: Story = {
render: (args) => ({
components: { AbSearch },
setup() {
return { args };
},
template: '<ab-search v-bind="args" />',
}),
};

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import { Search } from '@icon-park/vue-next';
const props = withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
value: '',
placeholder: '',
}
);
const emit = defineEmits(['update:value', 'click-search']);
function onInput(e: Event) {
const input = e.target as HTMLInputElement;
emit('update:value', input.value);
}
function onSearch() {
emit('click-search', props.value);
}
</script>
<template>
<div
bg="#7752B4"
text-white
fx-cer
rounded-12px
h-36px
px-12px
space-x-12px
w-276px
focus-within:w-396px
transition-width
>
<Search
theme="outline"
size="24"
fill="#fff"
is-btn
btn-click
@click="onSearch"
/>
<input
type="text"
:value="value"
:placeholder="placeholder"
input-reset
@input="onInput"
@keyup.enter="onSearch"
/>
</div>
</template>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbSelect from './ab-select.vue';
const meta: Meta<typeof AbSelect> = {
title: 'basic/ab-select',
component: AbSelect,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbSelect>;
export const Template: Story = {
render: (args) => ({
components: { AbSelect },
setup() {
return { args };
},
template: '<ab-select v-bind="args" />',
}),
};

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
} from '@headlessui/vue';
import { Down, Up } from '@icon-park/vue-next';
import { isObject, isString } from 'lodash';
import type { SelectItem } from '#/components';
const props = withDefaults(
defineProps<{
modelValue?: SelectItem | string;
items: Array<SelectItem | string>;
}>(),
{}
);
const emit = defineEmits(['update:modelValue']);
const selected = ref<SelectItem | string>(
props.modelValue || (props.items?.[0] ?? '')
);
const otherItems = computed(() => {
return (
props.items.filter((e) => {
if (isString(e) && isString(selected.value)) {
return e !== selected.value;
} else if (isObject(e) && isObject(selected.value)) {
return e.id !== selected.value.id;
} else {
return false;
}
}) ?? []
);
});
const label = computed(() => {
if (isString(selected.value)) {
return selected.value;
} else {
return selected.value.label ?? selected.value.value;
}
});
function getLabel(item: SelectItem | string) {
if (isString(item)) {
return item;
} else {
return item.label ?? item.value;
}
}
function getDisabled(item: SelectItem | string) {
return isString(item) ? false : item.disabled;
}
watchEffect(() => {
emit('update:modelValue', selected.value);
});
</script>
<template>
<Listbox v-slot="{ open }" v-model="selected">
<div
rel
flex="inline col"
rounded-6px
border="1px black"
text-main
py-4px
px-12px
>
<ListboxButton bg-transparent fx-cer justify-between space-x-24px>
<div>
{{ label }}
</div>
<div :class="[{ hidden: open }]">
<Down />
</div>
</ListboxButton>
<ListboxOptions mt-8px>
<div flex="~ items-end" justify-between space-x-24px>
<div flex="~ col" space-y-8px>
<ListboxOption
v-for="item in otherItems"
v-slot="{ active }"
:key="isString(item) ? item : item.id"
:value="item"
:disabled="getDisabled(item)"
>
<div
:class="[
{ 'text-primary': active },
getDisabled(item) ? 'is-disabled' : 'is-btn',
]"
>
{{ getLabel(item) }}
</div>
</ListboxOption>
</div>
<div :class="[{ hidden: !open }]"><Up /></div>
</div>
</ListboxOptions>
</div>
</Listbox>
</template>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbStatus from './ab-status.vue';
const meta: Meta<typeof AbStatus> = {
title: 'basic/ab-status',
component: AbStatus,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbStatus>;
export const Template: Story = {
render: (args) => ({
components: { AbStatus },
setup() {
return { args };
},
template: '<ab-status v-bind="args" />',
}),
};

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
running: boolean;
}>(),
{
running: false,
}
);
</script>
<template>
<div wh-24px f-cer>
<div rounded="1/2" f-cer border="2px solid white" wh-22px>
<div
:class="[running ? 'bg-running' : 'bg-stopped']"
rounded="1/2"
wh-10px
transition-colors
></div>
</div>
</div>
</template>

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AbSwitch from './ab-switch.vue';
const meta: Meta<typeof AbSwitch> = {
title: 'basic/ab-switch',
component: AbSwitch,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbSwitch>;
export const Template: Story = {
render: (args) => ({
components: { AbSwitch },
setup() {
return { args };
},
template: '<ab-switch v-bind="args" />',
}),
};

View File

@@ -0,0 +1,74 @@
<script lang="ts" setup>
import { Switch } from '@headlessui/vue';
const checked = defineModel<boolean>('checked', {
default: false,
});
</script>
<template>
<Switch v-model="checked" as="template">
<div
is-btn
w-48px
h-28px
rounded-full
rel
flex="inline items-center"
transition-colors
duration-300
p-3px
shadow="~ inset"
class="box"
:class="{ checked }"
>
<div
wh-22px
rounded="1/2"
transition-all
duration-300
class="slider"
:class="{ checked, 'translate-x-20px': checked }"
></div>
</div>
</Switch>
</template>
<style lang="scss" scope>
$bg-unchecked: #929292;
$bg-checked: #e7e7e7;
$slider-unchecked: #ececef;
$slider-unchecked-hover: #dbd8ec;
$slider-checked: #1c1259;
$slider-checked-hover: #62589e;
.box {
background: $bg-unchecked;
&.checked {
background: $bg-checked;
}
&:hover .slider {
&:not(.checked) {
background: $slider-unchecked-hover;
}
&.checked {
background: $slider-checked-hover;
}
}
}
.slider {
&:not(.checked) {
background: $slider-unchecked;
}
&.checked {
background: $slider-checked;
}
}
</style>

View File

@@ -0,0 +1,145 @@
<script lang="ts" setup>
import {
Calendar,
Download,
Home,
Log,
Logout,
MenuUnfold,
Play,
SettingTwo,
} from '@icon-park/vue-next';
const props = withDefaults(
defineProps<{
open?: boolean;
}>(),
{
open: false,
}
);
const show = ref(props.open);
const toggle = () => (show.value = !show.value);
const route = useRoute();
const { logout } = useAuth();
const items = [
{
id: 1,
icon: Home,
label: 'HomePage',
path: '/bangumi',
},
{
id: 2,
icon: Calendar,
label: 'Calendar',
path: '/calendar',
hidden: true,
},
{
id: 3,
icon: Play,
label: 'Player',
path: '/player',
},
{
id: 4,
icon: Download,
label: 'Downloader',
path: '/downloader',
hidden: true,
},
{
id: 5,
icon: Log,
label: 'Log',
path: '/log',
},
{
id: 6,
icon: SettingTwo,
label: 'Config',
path: '/config',
},
];
</script>
<template>
<div
:class="[show ? 'w-240px' : 'w-72px']"
bg-theme-col
text-white
transition-width
pb-12px
rounded-12px
>
<div overflow-hidden wh-full flex="~ col">
<div
w-full
h-60px
is-btn
f-cer
rounded-t-10px
bg="#E7E7E7"
text="#2A1C52"
rel
@click="toggle"
>
<div :class="[!show && 'abs opacity-0']" transition-opacity>
<div text-h1>Menu</div>
</div>
<MenuUnfold
theme="outline"
size="24"
fill="#2A1C52"
abs
left="24px"
:class="[show && 'rotate-y-180']"
/>
</div>
<RouterLink
v-for="i in items"
:key="i.id"
:to="i.path"
replace
:title="i.label"
fx-cer
px-24px
space-x-42px
h-48px
is-btn
transition-colors
hover:bg="#F1F5FA"
hover:text="#2A1C52"
:class="[
route.path === i.path && 'bg-[#F1F5FA] text-[#2A1C52]',
i.hidden && 'hidden',
]"
>
<Component :is="i.icon" :size="24" />
<div text-h2>{{ i.label }}</div>
</RouterLink>
<div
title="logout"
mt-auto
fx-cer
px-24px
space-x-42px
h-48px
is-btn
transition-colors
hover:bg="#F1F5FA"
hover:text="#2A1C52"
@click="logout"
>
<Logout :size="24" />
<div text-h2>Logout</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script lang="ts" setup>
import { Me, Pause, PlayOne, Power, Refresh } from '@icon-park/vue-next';
const search = ref('');
const show = ref(false);
const showAdd = ref(false);
const { onUpdate, offUpdate, start, pause, shutdown, restart } =
useProgramStore();
const { running } = storeToRefs(useProgramStore());
const items = [
{
id: 1,
label: 'Start',
icon: PlayOne,
handle: start,
},
{
id: 2,
label: 'Pause',
icon: Pause,
handle: pause,
},
{
id: 3,
label: 'Restart',
icon: Refresh,
handle: restart,
},
{
id: 4,
label: 'Shutdown',
icon: Power,
handle: shutdown,
},
{
id: 5,
label: 'Profile',
icon: Me,
handle: () => {
show.value = true;
},
},
];
onBeforeMount(() => {
onUpdate();
});
onUnmounted(() => {
offUpdate();
});
</script>
<template>
<div h-60px bg-theme-row text-white rounded-12px fx-cer px-24px>
<div text-h1 mr-12px>AutoBangumi</div>
<ab-search v-model:value="search" hidden />
<div ml-auto>
<ab-status-bar
:items="items"
:running="running"
@click-add="() => (showAdd = true)"
></ab-status-bar>
</div>
<ab-change-account v-model:show="show"></ab-change-account>
<ab-add-bangumi v-model:show="showAdd"></ab-add-bangumi>
</div>
</template>

View File

@@ -0,0 +1,76 @@
<script lang="ts" setup>
import type { Downloader, DownloaderType } from '#/config';
import type { SettingItem } from '#/components';
const { getSettingGroup } = useConfigStore();
const downloader = getSettingGroup('downloader');
const downloaderType: DownloaderType = ['qbittorrent'];
const items: SettingItem<Downloader>[] = [
{
configKey: 'type',
label: 'Downloader Type',
type: 'select',
css: 'w-115px',
prop: {
items: downloaderType,
},
},
{
configKey: 'host',
label: 'Host',
type: 'input',
prop: {
type: 'text',
placeholder: '127.0.0.1:8989',
},
},
{
configKey: 'username',
label: 'Username',
type: 'input',
prop: {
type: 'text',
placeholder: 'admin',
},
},
{
configKey: 'password',
label: 'Password',
type: 'input',
prop: {
type: 'text',
placeholder: 'admindmin',
},
bottomLine: true,
},
{
configKey: 'path',
label: 'Download Path',
type: 'input',
prop: {
type: 'text',
placeholder: '/downloads/Bangumi',
},
},
{
configKey: 'ssl',
label: 'SSL',
type: 'switch',
},
];
</script>
<template>
<ab-fold-panel title="Downloader Setting">
<div space-y-12px>
<ab-setting
v-for="i in items"
:key="i.configKey"
v-bind="i"
v-model:data="downloader[i.configKey]"
></ab-setting>
</div>
</ab-fold-panel>
</template>

View File

@@ -0,0 +1,54 @@
<script lang="ts" setup>
import type { BangumiManage, RenameMethod } from '#/config';
import type { SettingItem } from '#/components';
const { getSettingGroup } = useConfigStore();
const manage = getSettingGroup('bangumi_manage');
const renameMethod: RenameMethod = ['normal', 'pn', 'advance', 'none'];
const items: SettingItem<BangumiManage>[] = [
{
configKey: 'enable',
label: 'Enable',
type: 'switch',
},
{
configKey: 'rename_method',
label: 'Rename Method',
type: 'select',
prop: {
items: renameMethod,
},
bottomLine: true,
},
{
configKey: 'eps_complete',
label: 'Eps complete',
type: 'switch',
},
{
configKey: 'group_tag',
label: 'Add Group Tag',
type: 'switch',
},
{
configKey: 'remove_bad_torrent',
label: 'Delete Bad Torrent',
type: 'switch',
},
];
</script>
<template>
<ab-fold-panel title="Manage Setting">
<div space-y-12px>
<ab-setting
v-for="i in items"
:key="i.configKey"
v-bind="i"
v-model:data="manage[i.configKey]"
></ab-setting>
</div>
</ab-fold-panel>
</template>

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import type { Log, Program } from '#/config';
import type { SettingItem } from '#/components';
const { getSettingGroup } = useConfigStore();
const program = getSettingGroup('program');
const log = getSettingGroup('log');
const programItems: SettingItem<Program>[] = [
{
configKey: 'rss_time',
label: 'Interval Time of Rss',
type: 'input',
css: 'w-72px',
prop: {
type: 'number',
placeholder: 'port',
},
},
{
configKey: 'rename_time',
label: 'Interval Time of Rename',
type: 'input',
css: 'w-72px',
prop: {
type: 'number',
placeholder: 'port',
},
},
{
configKey: 'webui_port',
label: 'WebUI Port',
type: 'input',
css: 'w-72px',
prop: {
type: 'number',
placeholder: 'port',
},
bottomLine: true,
},
];
const logItems: SettingItem<Log> = {
configKey: 'debug_enable',
label: 'Debug',
type: 'switch',
};
</script>
<template>
<ab-fold-panel title="Normal Setting">
<div space-y-12px>
<ab-setting
v-for="i in programItems"
:key="i.configKey"
v-bind="i"
v-model:data="program[i.configKey]"
></ab-setting>
<ab-setting
v-bind="logItems"
v-model:data="log[logItems.configKey]"
></ab-setting>
</div>
</ab-fold-panel>
</template>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { Notification, NotificationType } from '#/config';
import type { SettingItem } from '#/components';
const { getSettingGroup } = useConfigStore();
const notification = getSettingGroup('notification');
const notificationType: NotificationType = ['telegram', 'server-chan', 'bark'];
const items: SettingItem<Notification>[] = [
{
configKey: 'enable',
label: 'Enable',
type: 'switch',
bottomLine: true,
},
{
configKey: 'type',
label: 'Type',
type: 'select',
css: 'w-140px',
prop: {
items: notificationType,
},
},
{
configKey: 'token',
label: 'Token',
type: 'input',
prop: {
type: 'text',
placeholder: 'token',
},
},
{
configKey: 'chat_id',
label: 'Chat ID',
type: 'input',
prop: {
type: 'text',
placeholder: 'chat id',
},
},
];
</script>
<template>
<ab-fold-panel title="Notification Setting">
<div space-y-12px>
<ab-setting
v-for="i in items"
:key="i.configKey"
v-bind="i"
v-model:data="notification[i.configKey]"
></ab-setting>
</div>
</ab-fold-panel>
</template>

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type {
RssParser,
RssParserLang,
RssParserMethodType,
RssParserType,
} from '#/config';
import type { SettingItem } from '#/components';
const { getSettingGroup } = useConfigStore();
const parser = getSettingGroup('rss_parser');
const sourceItems: RssParserType = ['mikan'];
const langs: RssParserLang = ['zh', 'en', 'jp'];
/** @ts-expect-error Incorrect order */
const parserMethods: RssParserMethodType = ['tmdb', 'mikan', 'parser'];
const items: SettingItem<RssParser>[] = [
{
configKey: 'enable',
label: 'Enable',
type: 'switch',
},
{
configKey: 'type',
label: 'Source',
type: 'select',
css: 'w-115px',
prop: {
items: sourceItems,
},
},
{
configKey: 'token',
label: 'Token',
type: 'input',
prop: {
type: 'text',
placeholder: 'token',
},
},
{
configKey: 'custom_url',
label: 'Custom Url',
type: 'input',
prop: {
type: 'text',
placeholder: 'mikanime.tv',
},
bottomLine: true,
},
{
configKey: 'language',
label: 'Language',
type: 'select',
prop: {
items: langs,
},
},
{
configKey: 'parser_type',
label: 'Parser Type',
type: 'select',
prop: {
items: parserMethods,
},
},
{
configKey: 'filter',
label: 'Exclude',
type: 'dynamic-tags',
},
];
</script>
<template>
<ab-fold-panel title="Parser Setting">
<div space-y-12px>
<ab-setting
v-for="i in items"
:key="i.configKey"
v-bind="i"
v-model:data="parser[i.configKey]"
></ab-setting>
</div>
</ab-fold-panel>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
const { types, type, url } = storeToRefs(usePlayerStore());
</script>
<template>
<ab-fold-panel title="Media Player Setting">
<div space-y-12px>
<ab-setting
v-model:data="type"
type="select"
label="type"
:prop="{ items: types }"
></ab-setting>
<ab-setting
v-model:data="url"
type="input"
label="url"
:prop="{ placeholder: 'media player url' }"
></ab-setting>
</div>
</ab-fold-panel>
</template>

View File

@@ -0,0 +1,75 @@
<script lang="ts" setup>
import type { Proxy, ProxyType } from '#/config';
import type { SettingItem } from '#/components';
const { getSettingGroup } = useConfigStore();
const proxy = getSettingGroup('proxy');
const proxyType: ProxyType = ['http', 'https', 'socks5'];
const items: SettingItem<Proxy>[] = [
{
configKey: 'enable',
label: 'Enable',
type: 'switch',
},
{
configKey: 'type',
label: 'Proxy Type',
type: 'select',
prop: {
items: proxyType,
},
bottomLine: true,
},
{
configKey: 'host',
label: 'Host',
type: 'input',
prop: {
type: 'text',
placeholder: '127.0.0.1',
},
},
{
configKey: 'port',
label: 'Port',
type: 'input',
prop: {
type: 'text',
placeholder: '7890',
},
},
{
configKey: 'username',
label: 'Username',
type: 'input',
prop: {
type: 'text',
placeholder: 'username',
},
},
{
configKey: 'password',
label: 'Password',
type: 'input',
prop: {
type: 'text',
placeholder: 'password',
},
},
];
</script>
<template>
<ab-fold-panel title="Proxy Setting">
<div space-y-12px>
<ab-setting
v-for="i in items"
:key="i.configKey"
v-bind="i"
v-model:data="proxy[i.configKey]"
></ab-setting>
</div>
</ab-fold-panel>
</template>

58
src/hooks/useApi.ts Normal file
View File

@@ -0,0 +1,58 @@
type AnyAsyncFuntion<TData = any> = (...args: any[]) => Promise<TData>;
type UnPromisify<T> = T extends AnyAsyncFuntion<infer U> ? U : never;
export function useApi<
TError = any,
TApi extends AnyAsyncFuntion = AnyAsyncFuntion,
TData = UnPromisify<TApi>
>(
api: TApi,
options?: {
failRule?: (data: TData) => boolean;
message?: {
success?: string;
fail?: string;
error?: string;
};
}
) {
const data = ref<TData>();
const isLoading = ref(false);
const fetchResult = createEventHook<TData>();
const fetchError = createEventHook<TError>();
const message = useMessage();
function execute(...params: Parameters<TApi>) {
isLoading.value = true;
api(...params)
.then((res: TData) => {
data.value = res;
fetchResult.trigger(res);
if (options?.failRule && options.failRule(res)) {
options.message?.fail && message.error(options.message.fail);
} else {
options?.message?.success && message.success(options.message.success);
}
})
.catch((err: TError) => {
fetchError.trigger(err);
options?.message?.error && message.error(options.message.error);
})
.finally(() => {
isLoading.value = false;
});
}
return {
data,
isLoading,
execute,
onResult: fetchResult.on,
onError: fetchError.on,
};
}

119
src/hooks/useAuth.ts Normal file
View File

@@ -0,0 +1,119 @@
import type { User } from '#/auth';
import type { ApiError } from '#/error';
export const useAuth = createSharedComposable(() => {
const auth = useLocalStorage('auth', '');
const message = useMessage();
const user = reactive<User>({
username: '',
password: '',
});
const isLogin = computed(() => auth.value !== '');
function clearUser() {
user.username = '';
user.password = '';
}
function check() {
if (user.username === '') {
message.warning('Please Enter Username!');
return false;
}
if (user.password === '') {
message.warning('Please Enter Password!');
return false;
}
return true;
}
function login() {
const { execute, onResult, onError } = useApi(apiAuth.login, {
message: {
success: 'Login Success!',
},
});
onResult((res) => {
auth.value = `${res.token_type} ${res.access_token}`;
clearUser();
});
onError((err) => {
const error = err as ApiError;
message.error(error.detail);
});
if (check()) {
execute(user.username, user.password);
}
}
const { execute: logout, onResult: onLogoutResult } = useApi(apiAuth.logout, {
failRule: (res) => !res,
message: {
success: 'Logout Success!',
fail: 'Logout Failed!',
},
});
onLogoutResult(() => {
clearUser();
auth.value = '';
});
const { execute: refresh, onResult: onRefreshResult } = useApi(
apiAuth.refresh,
{
message: {
success: 'Refresh Success!',
},
}
);
onRefreshResult((res) => {
auth.value = `${res.token_type} ${res.access_token}`;
});
function update() {
const { execute, onResult } = useApi(apiAuth.update, {
failRule: (res) => res.message !== 'update success',
message: {
success: 'Update Success!',
fail: 'Update Failed!',
},
});
onResult((res) => {
if (res.message === 'update success') {
auth.value = `${res.token_type} ${res.access_token}`;
clearUser();
} else {
user.password = '';
}
});
if (check()) {
if (user.password.length < 8) {
message.error('Password must be at least 8 characters long!');
} else {
execute(user.username, user.password);
}
}
}
return {
auth,
user,
isLogin,
login,
logout,
refresh,
update,
};
});

6
src/hooks/useMessage.ts Normal file
View File

@@ -0,0 +1,6 @@
import { createDiscreteApi } from 'naive-ui';
export const useMessage = createSharedComposable(() => {
const { message } = createDiscreteApi(['message']);
return message;
});

View File

@@ -1,14 +1,11 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import router from './router';
import { router } from './router';
import App from './App.vue';
import '@unocss/reset/tailwind-compat.css';
import 'virtual:uno.css';
import 'element-plus/es/components/message/style/css';
import 'element-plus/es/components/message-box/style/css';
const pinia = createPinia();
const app = createApp(App);

View File

@@ -1,56 +0,0 @@
<script lang="ts" setup>
import YMenu from './YMenu.vue';
const { status } = storeToRefs(programStore());
</script>
<template>
<div class="app-layout" w-full h-screen overflow-hidden flex>
<el-container>
<el-header
class="header"
flex="~ items-center justify-center"
h-65px
relative
>
<img src="@/assets/logo.png" alt="logo" class="h-7/10" />
<div absolute right-5 flex="~ items-center" text-3>
运行状态:
<div
class="i-carbon:dot-mark"
:class="[status ? 'text-green' : 'text-red']"
></div>
</div>
</el-header>
<el-container overflow-hidden>
<el-aside width="auto">
<YMenu />
</el-aside>
<el-main>
<el-scrollbar>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</div>
</template>
<style lang="scss" scope>
.app-layout {
@media screen and (max-width: 980px) {
font-size: 14px;
}
}
.header {
border-bottom: 1px solid var(--el-border-color);
}
</style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
const { fullPath } = useRoute();
const { width } = useWindowSize();
const isCollapse = ref(false);
const WIDTH = 980;
watchEffect(() => {
if (width.value < WIDTH) {
isCollapse.value = true;
} else {
isCollapse.value = false;
}
});
const items = [
{
icon: 'i-carbon-home',
title: '番剧管理',
url: '/bangumi',
},
{
icon: 'i-carbon-debug',
title: '调试',
url: '/debug',
},
{
icon: 'i-carbon:align-box-middle-right',
title: '日志',
url: '/log',
},
{
icon: 'i-carbon:settings',
title: '配置',
url: '/config',
},
];
</script>
<template>
<el-menu :default-active="fullPath" :collapse="isCollapse" router h-full>
<template v-for="(i, index) in items" :key="index">
<el-menu-item :index="i.url">
<div :class="[i.icon]" mr-0.5em></div>
<template #title>{{ i.title }}</template>
</el-menu-item>
</template>
</el-menu>
</template>

View File

@@ -1,45 +0,0 @@
<script setup lang="ts">
import { addBangumi } from '@/api/bangumi';
const props = defineProps<{
type: string;
}>();
const rssLink = ref('');
const loading = ref(false);
const dialog = ref();
const dialogData = ref(null);
async function add() {
loading.value = true;
const res = await addBangumi(props.type, rssLink.value);
if (res) {
loading.value = false;
dialogData.value = res.data;
dialog.value.open();
}
}
</script>
<template>
<ShowResults ref="dialog" title="执行结果" :results="dialogData" />
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span v-if="type === 'new'">订阅新番</span>
<span v-else-if="type === 'old'">订阅旧番</span>
</div>
</template>
<div class="card-con">
<el-input v-model="rssLink" placeholder="请输入番剧的rss链接">
<template #append>
<el-button type="primary" :loading="loading" @click="add"
>订阅</el-button
>
</template>
</el-input>
</div>
</el-card>
</template>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
const store = bangumiStore();
const activeName = ref('1');
const dialogData = ref(null);
const dialog = ref();
onActivated(() => {
store.get();
});
</script>
<template>
<ShowResults ref="dialog" title="执行结果" :results="dialogData" />
<div class="bangumi-data">
<el-collapse v-model="activeName" accordion>
<el-collapse-item title="已订阅番剧" name="1">
<span class="tips"
>: 目前只能管理 mikan , 如通过 api 添加其他来源的新番将
<b>不会</b> 出现在此处</span
>
<el-table
:data="store.data"
stripe
border
style="width: 100%"
max-height="40vh"
>
<el-table-column prop="official_title" label="番名" min-width="250" />
<el-table-column prop="season" label="季度" width="60" />
<el-table-column prop="dpi" label="分辨率" />
<el-table-column prop="subtitle" label="字幕" />
<el-table-column prop="group_name" label="字幕组" />
</el-table>
</el-collapse-item>
</el-collapse>
</div>
</template>
<style lang="scss" scope>
.bangumi-data {
.tips {
line-height: 2;
color: #f56c6c;
display: inline-block;
margin-bottom: 10px;
}
}
</style>

View File

@@ -1,38 +0,0 @@
<script lang="ts" setup>
import AddBangumi from './components/AddBangumi.vue';
import BangumiData from './components/BangumiData.vue';
</script>
<template>
<section class="bangumi">
<el-row>
<!-- S 番剧列表 -->
<el-col>
<BangumiData />
</el-col>
<!-- E 番剧列表 -->
</el-row>
<el-row :gutter="20" style="display: none">
<!-- S 添加新番 -->
<el-col :xs="24" :sm="24" :md="12" :lg="8" mb-20px>
<AddBangumi type="new" />
</el-col>
<!-- E 添加新番 -->
<!-- S 添加旧番 -->
<el-col :xs="24" :sm="24" :md="12" :lg="8" mb-20px>
<AddBangumi type="old" />
</el-col>
<!-- E 添加旧番 -->
</el-row>
</section>
</template>
<style lang="scss" scope>
.el-row {
&:not(:last-child) {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,64 +0,0 @@
<script lang="ts" setup>
import { form, renameMethod, tfOptions } from '../form-data';
const bgmManage = computed(() => form.bangumi_manage);
</script>
<template>
<ConfigFormRow title="番剧管理">
<ConfigFormCol label="启用">
<el-select v-model="bgmManage.enable" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="历史番剧下载">
<el-select v-model="bgmManage.eps_complete" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="重命名方法">
<el-select v-model="bgmManage.rename_method" flex-1>
<el-option
v-for="opt in renameMethod"
:key="opt"
:label="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="下载规则中添加组名">
<el-select v-model="bgmManage.group_tag" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="删除坏种">
<el-select v-model="bgmManage.remove_bad_torrent" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,46 +0,0 @@
<script lang="ts" setup>
import { downloaderType, form, tfOptions } from '../form-data';
const downloader = computed(() => form.downloader);
</script>
<template>
<ConfigFormRow title="下载器设置">
<ConfigFormCol label="下载器">
<el-select v-model="downloader.type" flex-1>
<el-option
v-for="opt in downloaderType"
:key="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="下载器地址" prop="downloader.host">
<el-input v-model="downloader.host"></el-input>
</ConfigFormCol>
<ConfigFormCol label="下载路径(映射后)">
<el-input v-model="downloader.path"></el-input>
</ConfigFormCol>
<ConfigFormCol label="用户名">
<el-input v-model="downloader.username"></el-input>
</ConfigFormCol>
<ConfigFormCol label="密码">
<el-input v-model="downloader.password"></el-input>
</ConfigFormCol>
<ConfigFormCol label="ssl">
<el-select v-model="downloader.ssl" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,20 +0,0 @@
<script lang="ts" setup>
import { form, tfOptions } from '../form-data';
const log = computed(() => form.log);
</script>
<template>
<ConfigFormRow title="日志">
<ConfigFormCol label="启用debug">
<el-select v-model="log.debug_enable" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,39 +0,0 @@
<script lang="ts" setup>
import { form, notificationType, tfOptions } from '../form-data';
const notification = computed(() => form.notification);
</script>
<template>
<ConfigFormRow title="通知">
<ConfigFormCol label="启用">
<el-select v-model="notification.enable" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="通知类型">
<el-select v-model="notification.type" flex-1>
<el-option
v-for="opt in notificationType"
:key="opt"
:label="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="token">
<el-input v-model="notification.token"></el-input>
</ConfigFormCol>
<ConfigFormCol label="chat_id">
<el-input v-model="notification.chat_id" type="number"></el-input>
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,21 +0,0 @@
<script lang="ts" setup>
import { form } from '../form-data';
const program = computed(() => form.program);
</script>
<template>
<ConfigFormRow title="主程序">
<ConfigFormCol label="RSS 检查间隔">
<el-input v-model.number="program.rss_time" type="number" />
</ConfigFormCol>
<ConfigFormCol label="重命名频率">
<el-input v-model.number="program.rename_time" type="number" />
</ConfigFormCol>
<ConfigFormCol label="WebUI 端口" prop="program.webui_port">
<el-input v-model.number="program.webui_port" type="number" />
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,51 +0,0 @@
<script lang="ts" setup>
import { form, proxyType, tfOptions } from '../form-data';
const proxy = computed(() => form.proxy);
</script>
<template>
<ConfigFormRow title="代理">
<ConfigFormCol label="启用">
<el-select v-model="proxy.enable" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="代理类型">
<el-select v-model="proxy.type" flex-1>
<el-option
v-for="opt in proxyType"
:key="opt"
:label="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="host" prop="proxy.host">
<el-input v-model="proxy.host"></el-input>
</ConfigFormCol>
<ConfigFormCol label="port" prop="proxy.port">
<el-input v-model.number="proxy.port" type="number"></el-input>
</ConfigFormCol>
<ConfigFormCol label="username">
<el-input v-model="proxy.username"></el-input>
</ConfigFormCol>
<ConfigFormCol label="password">
<el-input
v-model="proxy.password"
type="password"
show-password
></el-input>
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,74 +0,0 @@
<script lang="ts" setup>
import {
form,
rssParserLang,
rssParserMethodType,
rssParserType,
tfOptions,
} from '../form-data';
const rssParser = computed(() => form.rss_parser);
const filter = ref('');
watch(filter, (nv) => {
const value = nv.split(',').filter((e) => e !== '');
rssParser.value.filter = value;
});
</script>
<template>
<ConfigFormRow title="RSS 解析">
<ConfigFormCol label="启用">
<el-select v-model="rssParser.enable" flex-1>
<el-option
v-for="(opt, index) in tfOptions"
:key="index"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="源">
<el-select v-model="rssParser.type" flex-1>
<el-option
v-for="opt in rssParserType"
:key="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="token">
<el-input v-model="rssParser.token"></el-input>
</ConfigFormCol>
<ConfigFormCol label="语言">
<el-select v-model="rssParser.language" flex-1>
<el-option
v-for="opt in rssParserLang"
:key="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="源站链接">
<el-input v-model="rssParser.custom_url"></el-input>
</ConfigFormCol>
<ConfigFormCol label="解析类型">
<el-select v-model="rssParser.parser_type" flex-1>
<el-option
v-for="opt in rssParserMethodType"
:key="opt"
:value="opt"
></el-option>
</el-select>
</ConfigFormCol>
<ConfigFormCol label="筛选">
<el-input v-model="filter" :value="rssParser.filter.join(',')"></el-input>
</ConfigFormCol>
</ConfigFormRow>
</template>

View File

@@ -1,79 +0,0 @@
import type {
Config,
DownloaderType,
NotificationType,
ProxyType,
RenameMethod,
RssParserLang,
RssParserMethodType,
RssParserType,
} from '#/config';
export const form = reactive<Config>({
program: {
rss_time: 0,
rename_time: 0,
webui_port: 0,
},
downloader: {
type: 'qbittorrent',
host: '',
username: '',
password: '',
path: '',
ssl: false,
},
rss_parser: {
enable: true,
type: 'mikan',
token: '',
custom_url: '',
filter: [],
language: 'zh',
parser_type: 'parser',
},
bangumi_manage: {
enable: true,
eps_complete: true,
rename_method: 'normal',
group_tag: true,
remove_bad_torrent: true,
},
log: {
debug_enable: false,
},
proxy: {
enable: false,
type: 'http',
host: '',
port: 0,
username: '',
password: '',
},
notification: {
enable: false,
type: 'telegram',
token: '',
chat_id: '',
},
});
export const downloaderType: DownloaderType = ['qbittorrent'];
export const rssParserType: RssParserType = ['mikan'];
export const rssParserMethodType: RssParserMethodType = [
'tmdb',
'mikan',
'parser',
];
export const rssParserLang: RssParserLang = ['zh', 'en', 'jp'];
export const renameMethod: RenameMethod = ['normal', 'pn', 'advance', 'none'];
export const proxyType: ProxyType = ['http', 'https', 'socks5'];
export const notificationType: NotificationType = [
'telegram',
'server-chan',
'bark',
];
export const tfOptions = [
{ label: '是', value: true },
{ label: '否', value: false },
];

View File

@@ -1,65 +0,0 @@
<script lang="ts" setup>
import ProgramItem from './components/ProgramItem.vue';
import DownloaderItem from './components/DownloaderItem.vue';
import RssParserItem from './components/RssParserItem.vue';
import BangumiManageItem from './components/BangumiManageItem.vue';
import LogItem from './components/LogItem.vue';
import ProxyItem from './components/ProxyItem.vue';
import NotificationItem from './components/NotificationItem.vue';
import { form } from './form-data';
const store = configStore();
function submit() {
store.set(form);
}
function formSync() {
if (store.config) {
Object.keys(store.config).forEach((key) => {
if (store.config) {
form[key] = JSON.parse(JSON.stringify(store.config[key]));
}
});
}
}
onActivated(async () => {
await store.get();
formSync();
});
</script>
<template>
<section class="settings" pb30>
<el-row :gutter="20">
<el-col :xs="24" :sm="24">
<el-form :model="form" label-position="right">
<el-collapse>
<ProgramItem />
<DownloaderItem />
<RssParserItem />
<BangumiManageItem />
<LogItem />
<ProxyItem />
<NotificationItem />
</el-collapse>
</el-form>
</el-col>
</el-row>
<div flex="~ items-center justify-center" mt20>
<el-button type="primary" @click="submit">保存</el-button>
<el-button @click="formSync">还原</el-button>
</div>
</section>
</template>
<style lang="scss" scope>
.el-row {
&:not(:last-child) {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,90 +0,0 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus';
import { resetRule } from '@/api/debug';
import { appRestart } from '@/api/program';
const loading = ref(false);
async function reset() {
loading.value = true;
const res = await resetRule();
loading.value = false;
if (res.data === 'Success') {
ElMessage({
message: '数据已重置, 建议重启程序或容器',
type: 'success',
});
} else {
ElMessage({
message: `错误: ${res.data}`,
type: 'error',
});
}
}
function restart() {
ElMessageBox.confirm('该操作将重启程序!', {
type: 'warning',
})
.then(() => {
appRestart()
.then((res) => {
if (res) {
ElMessage({
message: '重启中...',
type: 'success',
});
}
})
.catch((error) => {
console.error('🚀 ~ file: index.vue:41 ~ .then ~ error:', error);
ElMessage({
message: '操作失败, 请手动重启容器!',
type: 'error',
});
});
})
.catch(() => {});
}
</script>
<template>
<section class="debug">
<el-row :gutter="20">
<!-- S 重置数据 -->
<el-col :xs="24" :sm="12" :lg="8" mb-20px>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>重置AB数据</span>
</div>
</template>
<div class="card-con">
<el-button type="danger" :loading="loading" @click="reset"
>重置</el-button
>
</div>
</el-card>
</el-col>
<!-- E 重置数据 -->
<!-- S 重启程序 -->
<el-col :xs="24" :sm="12" :lg="8" mb-20px>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>重启程序</span>
</div>
</template>
<div class="card-con">
<el-button type="danger" :loading="loading" @click="restart"
>重启</el-button
>
</div>
</el-card>
</el-col>
<!-- E 重启程序 -->
</el-row>
</section>
</template>

28
src/pages/index.vue Normal file
View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
definePage({
name: 'Index',
redirect: '/bangumi',
});
</script>
<template>
<div layout-container>
<ab-topbar />
<main layout-main>
<ab-sidebar />
<div layout-content>
<ab-page-title :title="$route.name"></ab-page-title>
<div overflow-auto mt-12px flex-grow>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
</div>
</div>
</main>
</div>
</template>

View File

@@ -0,0 +1,92 @@
<script lang="ts" setup>
import type { BangumiRule } from '#/bangumi';
const { data } = storeToRefs(useBangumiStore());
const { getAll, useUpdateRule, useDisableRule, useEnableRule } =
useBangumiStore();
const editRule = reactive<{
show: boolean;
item: BangumiRule;
}>({
show: false,
item: {
added: false,
deleted: false,
dpi: '',
eps_collect: false,
filter: [],
group_name: '',
id: 0,
official_title: '',
offset: 0,
poster_link: '',
rss_link: [],
rule_name: '',
save_path: '',
season: 1,
season_raw: '',
source: null,
subtitle: '',
title_raw: '',
year: null,
},
});
function open(data: BangumiRule) {
editRule.show = true;
editRule.item = data;
}
function refresh() {
editRule.show = false;
getAll();
}
const { execute: updateRule, onResult: onUpdateRuleResult } = useUpdateRule();
const { execute: enableRule, onResult: onEnableRuleResult } = useEnableRule();
const { execute: disableRule, onResult: onDisableRuleResult } =
useDisableRule();
onUpdateRuleResult(() => {
refresh();
});
onDisableRuleResult(() => {
refresh();
});
onEnableRuleResult(() => {
refresh();
});
onActivated(() => {
getAll();
});
definePage({
name: 'Bangumi List',
});
</script>
<template>
<div flex="~ wrap" gap-y-12px gap-x-50px>
<ab-bangumi-card
v-for="i in data"
:key="i.id"
:class="[i.deleted && 'grayscale']"
:poster="i.poster_link ?? ''"
:name="i.official_title"
:season="i.season"
@click="() => open(i)"
></ab-bangumi-card>
<ab-edit-rule
v-model:show="editRule.show"
v-model:rule="editRule.item"
@enable="(id) => enableRule(id)"
@disable="({ id, deleteFile }) => disableRule(id, deleteFile)"
@apply="(rule) => updateRule(rule)"
></ab-edit-rule>
</div>
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
definePage({
name: 'Calendar',
});
</script>
<template>
<div>null</div>
</template>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
const { getConfig, setConfig } = useConfigStore();
getConfig();
definePage({
name: 'Config',
});
</script>
<template>
<div h-full flex="~ col">
<div grid="~ cols-2" gap-20px mb-auto>
<div space-y-20px>
<config-normal></config-normal>
<config-parser></config-parser>
<config-download></config-download>
<config-manage></config-manage>
</div>
<div space-y-20px>
<config-notification></config-notification>
<config-proxy></config-proxy>
<config-player></config-player>
</div>
</div>
<div fx-cer justify-end gap-8px mt-20px>
<ab-button type="warn" @click="getConfig">Cancel</ab-button>
<ab-button @click="setConfig">Apply</ab-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
definePage({
name: 'Downloader',
});
</script>
<template>
<div>null</div>
</template>

111
src/pages/index/log.vue Normal file
View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
const { onUpdate, offUpdate, reset, copy } = useLogStore();
const { log } = storeToRefs(useLogStore());
onActivated(() => {
onUpdate();
});
onDeactivated(() => {
offUpdate();
});
definePage({
name: 'Log',
});
</script>
<template>
<div flex="~ wrap" gap-12px>
<ab-container title="Log" w-660px grow>
<div
rounded-10px
border="1px solid black"
overflow-auto
p-10px
max-h-60vh
>
<pre text-main>{{ log }}</pre>
</div>
<div flex="~ justify-end" space-x-10px mt-12px>
<ab-button type="warn" size="small" @click="reset">Reset</ab-button>
<ab-button size="small" @click="copy">Copy</ab-button>
</div>
</ab-container>
<div grow w-500px space-y-20px>
<ab-container title="Contact Infomation">
<div space-y-12px>
<ab-label label="Github">
<ab-button
size="small"
link="https://github.com/EstrellaXD/Auto_Bangumi"
target="_blank"
>
Go
</ab-button>
</ab-label>
<ab-label label="WebUI Repo">
<ab-button
size="small"
link="https://github.com/Rewrite0/Auto_Bangumi_WebUI"
target="_blank"
>
Go
</ab-button>
</ab-label>
<div line></div>
<ab-label label="Twitter">
<ab-button
size="small"
link="https://twitter.com/Estrella_Pan"
target="_blank"
>
Go
</ab-button>
</ab-label>
<ab-label label="Telegram Group">
<ab-button
size="small"
link="https://t.me/autobangumi"
target="_blank"
>
Join
</ab-button>
</ab-label>
</div>
</ab-container>
<ab-container title="Bug Report">
<div space-y-12px>
<ab-button
mx-auto
text-16px
w-300px
h-46px
rounded-10px
link="https://github.com/EstrellaXD/Auto_Bangumi/issues"
>Github Issue</ab-button
>
<div line></div>
<ab-button
mx-auto
text-16px
w-300px
h-46px
rounded-10px
link="mailto:estrellaxd05@gmail.com"
>Email Contact</ab-button
>
</div>
</ab-container>
</div>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
definePage({
name: 'Player',
});
const { url } = storeToRefs(usePlayerStore());
</script>
<template>
<template v-if="url === ''">
<div wh-full f-cer text-h1 text-primary>
<RouterLink to="/config" hover:underline
>Please set up the media player</RouterLink
>
</div>
</template>
<iframe
v-else
:src="url"
frameborder="0"
allowfullscreen="true"
w-full
h-full
flex-1
rounded-12px
></iframe>
</template>

View File

@@ -1,18 +0,0 @@
<script setup lang="ts">
const store = logStore();
onActivated(() => {
store.get();
store.onUpdate();
});
onDeactivated(() => {
store.removeUpdate();
});
</script>
<template>
<div class="log-box" wh-full overflow-hidden px-2em leading-2em text-12px>
<pre whitespace-break-spaces>{{ store.log }}</pre>
</div>
</template>

40
src/pages/login.vue Normal file
View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
const { user, login } = useAuth();
definePage({
name: 'Login',
});
</script>
<template>
<div f-cer layout-container>
<ab-container title="Login" w-365px>
<div space-y-16px>
<ab-label label="Username">
<input
v-model="user.username"
type="text"
placeholder="username"
ab-input
/>
</ab-label>
<ab-label label="Password">
<input
v-model="user.password"
type="password"
placeholder="password"
ab-input
@keyup.enter="login"
/>
</ab-label>
<div line></div>
<div flex="~ justify-end">
<ab-button size="small" @click="login">Login</ab-button>
</div>
</div>
</ab-container>
</div>
</template>

148
src/router-type.d.ts vendored Normal file
View File

@@ -0,0 +1,148 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
/// <reference types="unplugin-vue-router/client" />
import type {
// type safe route locations
RouteLocationTypedList,
RouteLocationResolvedTypedList,
RouteLocationNormalizedTypedList,
RouteLocationNormalizedLoadedTypedList,
RouteLocationAsString,
RouteLocationAsRelativeTypedList,
RouteLocationAsPathTypedList,
// helper types
// route definitions
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
// vue-router extensions
_RouterTyped,
RouterLinkTyped,
RouterLinkPropsTyped,
NavigationGuard,
UseLinkFnTyped,
// data fetching
_DataLoader,
_DefineLoaderOptions,
} from 'unplugin-vue-router/types'
declare module 'vue-router/auto/routes' {
export interface RouteNamedMap {
'Index': RouteRecordInfo<'Index', '/', Record<never, never>, Record<never, never>>,
'Bangumi List': RouteRecordInfo<'Bangumi List', '/bangumi', Record<never, never>, Record<never, never>>,
'Calendar': RouteRecordInfo<'Calendar', '/calendar', Record<never, never>, Record<never, never>>,
'Config': RouteRecordInfo<'Config', '/config', Record<never, never>, Record<never, never>>,
'Downloader': RouteRecordInfo<'Downloader', '/downloader', Record<never, never>, Record<never, never>>,
'Log': RouteRecordInfo<'Log', '/log', Record<never, never>, Record<never, never>>,
'Player': RouteRecordInfo<'Player', '/player', Record<never, never>, Record<never, never>>,
'Login': RouteRecordInfo<'Login', '/login', Record<never, never>, Record<never, never>>,
}
}
declare module 'vue-router/auto' {
import type { RouteNamedMap } from 'vue-router/auto/routes'
export type RouterTyped = _RouterTyped<RouteNamedMap>
/**
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
| RouteLocationAsString<RouteNamedMap>
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
/**
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
*/
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
/**
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
*/
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
export function useRouter(): RouterTyped
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
export const useLink: UseLinkFnTyped<RouteNamedMap>
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
export const RouterLink: RouterLinkTyped<RouteNamedMap>
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
// Experimental Data Fetching
export function defineLoader<
P extends Promise<any>,
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
isLazy extends boolean = false,
>(
name: Name,
loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
options?: _DefineLoaderOptions<isLazy>,
): _DataLoader<Awaited<P>, isLazy>
export function defineLoader<
P extends Promise<any>,
isLazy extends boolean = false,
>(
loader: (route: RouteLocationNormalizedLoaded) => P,
options?: _DefineLoaderOptions<isLazy>,
): _DataLoader<Awaited<P>, isLazy>
export {
_definePage as definePage,
_HasDataLoaderMeta as HasDataLoaderMeta,
_setupDataFetchingGuard as setupDataFetchingGuard,
_stopDataFetchingScope as stopDataFetchingScope,
} from 'unplugin-vue-router/runtime'
}
declare module 'vue-router' {
import type { RouteNamedMap } from 'vue-router/auto/routes'
export interface TypesConfig {
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
beforeRouteLeave: NavigationGuard<RouteNamedMap>
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
$router: _RouterTyped<RouteNamedMap>
RouterLink: RouterLinkTyped<RouteNamedMap>
}
}

View File

@@ -1,40 +1,34 @@
import { createRouter, createWebHashHistory } from 'vue-router';
const YLayout = () => import('../pages/YLayout.vue');
const YBangumi = () => import('../pages/bangumi/index.vue');
const YDebug = () => import('../pages/debug/index.vue');
const YLog = () => import('../pages/journal/index.vue');
const YConfig = () => import('../pages/config/index.vue');
const routes = [
{
path: '/',
component: YLayout,
redirect: '/bangumi',
children: [
{
path: 'bangumi',
component: YBangumi,
},
{
path: 'debug',
component: YDebug,
},
{
path: 'log',
component: YLog,
},
{
path: 'config',
component: YConfig,
},
],
},
];
import { createRouter, createWebHashHistory } from 'vue-router/auto';
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
router.beforeEach((to) => {
const { isLogin } = useAuth();
const { type, url } = storeToRefs(usePlayerStore());
if (!isLogin.value && to.path !== '/login') {
return { name: 'Login' };
}
if (isLogin.value && to.path === '/login') {
return { name: 'Index' };
}
if (type.value === 'jump' && url.value !== '' && to.path === '/player') {
open(url.value);
return false;
}
watch(isLogin, (val) => {
if (to.path === '/login' && val) {
router.replace({ name: 'Index' });
}
if (to.path !== '/login' && !val) {
router.replace({ name: 'Login' });
}
});
});
export { router };

View File

@@ -1,13 +1,78 @@
import { getABData } from '../api/bangumi';
import type { BangumiItem } from '#/bangumi';
import type { BangumiRule } from '#/bangumi';
export const bangumiStore = defineStore('bangumi', () => {
const data = ref<BangumiItem[]>();
export const useBangumiStore = defineStore('bangumi', () => {
const data = ref<BangumiRule[]>();
const get = async () => {
const res = await getABData();
data.value = res.data;
};
function getAll() {
const { execute, onResult } = useApi(apiBangumi.getAll);
return { data, get };
function sort(arr: BangumiRule[]) {
return arr.sort((a, b) => b.id - a.id);
}
onResult((res) => {
const enabled = sort(res.filter((e) => !e.deleted));
const disabled = sort(res.filter((e) => e.deleted));
data.value = [...enabled, ...disabled];
});
execute();
}
function useUpdateRule() {
const { execute, onResult } = useApi(apiBangumi.updateRule, {
failRule: (data) => {
return data.status !== 'success';
},
message: {
success: 'Update Success!',
fail: 'Update Failed!',
error: 'Operation Failed!',
},
});
return {
execute,
onResult,
};
}
function useDisableRule() {
const { execute, onResult } = useApi(apiBangumi.disableRule, {
failRule: (data) => {
return data.status !== 'success';
},
message: {
success: 'Disabled Success!',
fail: 'Disabled Failed!',
error: 'Operation Failed!',
},
});
return {
execute,
onResult,
};
}
function useEnableRule() {
const { execute, onResult } = useApi(apiBangumi.enableRule, {
failRule: (data) => {
return data.status !== 'success';
},
message: {
success: 'Enabled Success!',
fail: 'Enabled Failed!',
error: 'Operation Failed!',
},
});
return {
execute,
onResult,
};
}
return { data, getAll, useUpdateRule, useDisableRule, useEnableRule };
});

View File

@@ -1,70 +1,34 @@
import { ElMessage, ElMessageBox } from 'element-plus';
import { getConfig, setConfig } from '@/api/config';
import { appRestart } from '@/api/program';
import type { Config } from '#/config';
import { type Config, initConfig } from '#/config';
const { status } = storeToRefs(programStore());
export const useConfigStore = defineStore('config', () => {
const config = ref<Config>(initConfig);
export const configStore = defineStore('config', () => {
const config = ref<Config>();
const { execute: getConfig, onResult: onGetConfigRusult } = useApi(
apiConfig.getConfig
);
const get = async () => {
config.value = await getConfig();
};
onGetConfigRusult((res) => {
config.value = res;
});
const set = async (newConfig: Omit<Config, 'data_version'>) => {
let finalConfig: Config;
if (config.value !== undefined) {
finalConfig = Object.assign(config.value, newConfig);
const res = await setConfig(finalConfig);
const { execute: set } = useApi(apiConfig.updateConfig, {
failRule: (res) => !res,
message: {
success: 'Apply Success!',
fail: 'Apply Failed!',
},
});
if (res) {
ElMessage({
message: '保存成功!',
type: 'success',
});
const setConfig = () => set(config.value);
if (!status.value) {
ElMessageBox.confirm('当前程序没有运行,是否重启?', {
type: 'warning',
})
.then(() => {
appRestart()
.then((res) => {
if (res) {
ElMessage({
message: '重启中...',
type: 'success',
});
}
})
.catch((error) => {
console.error(
'🚀 ~ file: index.vue:41 ~ .then ~ error:',
error
);
ElMessage({
message: '操作失败, 请手动重启容器!',
type: 'error',
});
});
})
.catch(() => {});
}
} else {
ElMessage({
message: '保存失败, 请重试!',
type: 'error',
});
}
}
return false;
};
function getSettingGroup<Tkey extends keyof Config>(key: Tkey) {
return computed<Config[Tkey]>(() => config.value[key]);
}
return {
get,
set,
config,
getConfig,
setConfig,
getSettingGroup,
};
});

View File

@@ -1,25 +1,51 @@
import { getABLog } from '../api/debug';
export const logStore = defineStore('log', () => {
export const useLogStore = defineStore('log', () => {
const log = ref('');
const timer = ref<NodeJS.Timer | null>(null);
const { auth } = useAuth();
const message = useMessage();
const get = async () => {
log.value = await getABLog();
};
function get() {
const { execute, onResult } = useApi(apiLog.getLog);
const onUpdate = () => {
timer.value = setInterval(() => get(), 3000);
};
onResult((value) => {
log.value = value;
});
const removeUpdate = () => {
clearInterval(Number(timer.value));
};
if (auth.value !== '') {
execute();
}
}
const { execute: reset, onResult: onClearLogResult } = useApi(
apiLog.clearLog
);
onClearLogResult((res) => {
if (res) {
log.value = '';
}
});
const { pause: offUpdate, resume: onUpdate } = useIntervalFn(get, 3000, {
immediate: false,
immediateCallback: true,
});
function copy() {
const { copy: copyLog, isSupported } = useClipboard({ source: log });
if (isSupported) {
copyLog();
message.success('Copy Success!');
} else {
message.error('Your browser does not support Clipboard API!');
}
}
return {
log,
get,
reset,
onUpdate,
removeUpdate,
offUpdate,
copy,
};
});

13
src/store/player.ts Normal file
View File

@@ -0,0 +1,13 @@
type MediaPlayerType = 'jump' | 'iframe';
export const usePlayerStore = defineStore('player', () => {
const types = ref<MediaPlayerType[]>(['jump', 'iframe']);
const type = useLocalStorage<MediaPlayerType>('media-player-type', 'jump');
const url = useLocalStorage<string>('media-player-url', '');
return {
types,
type,
url,
};
});

View File

@@ -1,20 +1,52 @@
import { appStatus } from '../api/program';
export const useProgramStore = defineStore('program', () => {
const { auth } = useAuth();
const running = ref(false);
export const programStore = defineStore('program', () => {
const status = ref(false);
const timer = ref<NodeJS.Timer | null>(null);
function getStatus() {
const { execute, onResult } = useApi(apiProgram.status);
const getStatus = async () => {
status.value = await appStatus();
};
onResult((res) => {
running.value = res;
});
const onUpdate = () => {
timer.value = setInterval(() => getStatus(), 3000);
};
if (auth.value !== '') {
execute();
}
}
const { pause: offUpdate, resume: onUpdate } = useIntervalFn(
getStatus,
3000,
{
immediate: false,
immediateCallback: true,
}
);
function opts(handle: string) {
return {
failRule: (res: boolean) => !res,
message: {
success: `${handle} Success!`,
fail: `${handle} Failed!`,
},
};
}
const { execute: start } = useApi(apiProgram.start, opts('Start'));
const { execute: pause } = useApi(apiProgram.stop, opts('Pause'));
const { execute: shutdown } = useApi(apiProgram.shutdown, opts('Shutdown'));
const { execute: restart } = useApi(apiProgram.restart, opts('Restart'));
return {
status,
running,
getStatus,
onUpdate,
offUpdate,
start,
pause,
shutdown,
restart,
};
});

50
src/style/global.scss Normal file
View File

@@ -0,0 +1,50 @@
$scrollbar-color: #372a87;
:root {
--scrollbar-size: 6px;
--scrollbar-color: transparent;
--scrollbar-thumb-color: #{rgba($scrollbar-color, 0.5)};
--scrollbar-thumb-hover-color: #{rgba($scrollbar-color, 1)};
}
::-webkit-scrollbar {
width: var(--scrollbar-size);
height: var(--scrollbar-size);
}
/* 滚动槽--外层轨道 */
::-webkit-scrollbar-track {
background: var(--scrollbar-color);
}
/* 内层轨道(不包含滚动块部分) */
/* 透明度设置为全透明,使得滚动条背景色为网页颜色 */
::-webkit-scrollbar-track-piece {
opacity: 0;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: calc(var(--scrollbar-size) / 2);
background: var(--scrollbar-thumb-color);
&:hover {
background: var(--scrollbar-thumb-hover-color);
}
}
/* 滚动条按钮 */
::-webkit-scrollbar-button {
display: none;
}
/* 横向滚动条和纵向滚动条相交处尖角的颜色 */
::-webkit-scrollbar-corner {
background-color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

13
src/style/mixin.scss Normal file
View File

@@ -0,0 +1,13 @@
@mixin bg-mouse-event($normal, $hover, $active) {
background: $normal;
transition: background 0.3s;
&:hover {
background: $hover;
}
&:active {
transition: none;
background: $active;
}
}

16
src/style/transition.scss Normal file
View File

@@ -0,0 +1,16 @@
// transition
.fade {
&-enter-active,
&-leave-active {
transition: opacity 0.2s ease;
}
&-enter-from,
&-leave-to {
position: absolute;
opacity: 0;
}
}
// transition-group

43
src/utils/axios.ts Normal file
View File

@@ -0,0 +1,43 @@
import Axios from 'axios';
import type { ApiError } from '#/error';
export const axios = Axios.create();
axios.interceptors.request.use((config) => {
const { auth } = useAuth();
if (auth.value !== '' && config.headers) {
config.headers.Authorization = auth.value;
}
return config;
});
axios.interceptors.response.use(
(res) => {
return res;
},
(err) => {
const status = err.response.status as ApiError['status'];
const detail = err.response.data.detail as ApiError['detail'];
const error = {
status,
detail,
};
const message = useMessage();
/** token 过期 */
if (error.status === 401) {
const { auth } = useAuth();
auth.value = '';
}
if (error.status === 500) {
const msg = error.detail ? error.detail : 'Request Error!';
message.error(msg);
}
return Promise.reject(error);
}
);

View File

@@ -17,15 +17,14 @@
"paths": {
"@/*": ["src/*"],
"#/*": ["types/*"]
},
"types": ["element-plus/global"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/config.ts"
"types/**/*.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
}

18
types/auth.ts Normal file
View File

@@ -0,0 +1,18 @@
export interface LoginSuccess {
access_token: string;
token_type: string;
expire: number;
}
export interface Logout {
message: 'logout success';
}
export interface Update extends LoginSuccess {
message: 'update success';
}
export interface User {
username: string;
password: string;
}

View File

@@ -1,18 +1,21 @@
export interface BangumiItem {
export interface BangumiRule {
added: boolean;
deleted: boolean;
dpi: string;
eps_collect: boolean;
filter: string[];
group_name: string;
id: number;
official_title: string;
year: string | null;
title_raw: string;
offset: number;
poster_link: string | null;
rss_link: string[];
rule_name: string;
save_path: string;
season: number;
season_raw: string;
group_name: string;
dpi: string;
source: string;
source: string | null;
subtitle: string;
eps_collect: boolean;
offset: number;
filter: string[];
rss_link: string[];
poster_link: string;
added: boolean;
title_raw: string;
year: string | null;
}

Some files were not shown because too many files have changed in this diff Show More