Feat: movie list use dir (#59)

* Feat: add dir to playerlist

* Feat: update get movie list api

* Feat: movie list switch dir

* Feat: compatible emby

* Feat: movie list create dir

* Fix: fix add bilibili video path error

* Fix: fix add bilibili video path error

* Feat: new dir not use proxy

* Feat: clear dir

* Fix: fix extra request
This commit is contained in:
阿龙 2024-05-15 23:52:45 +08:00 committed by GitHub
parent 448bd355ef
commit faa3bac79d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 259 additions and 44 deletions

4
components.d.ts vendored
View File

@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
Alist: typeof import('./src/components/fileList/alist.vue')['default']
BilibiliParse: typeof import('./src/components/cinema/dialogs/bilibiliParse.vue')['default']
copy: typeof import('./src/components/icons/Play copy.vue')['default']
CopyButton: typeof import('./src/components/CopyButton.vue')['default']
CustomHeaders: typeof import('./src/components/cinema/dialogs/customHeaders.vue')['default']
CustomSubtitles: typeof import('./src/components/cinema/dialogs/customSubtitles.vue')['default']
@ -27,6 +28,7 @@ declare module 'vue' {
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIco: typeof import('element-plus/es')['ElIco']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
@ -51,8 +53,10 @@ declare module 'vue' {
Header: typeof import('./src/components/Header.vue')['default']
Moon: typeof import('./src/components/icons/Moon.vue')['default']
MovieList: typeof import('./src/components/cinema/MovieList.vue')['default']
MovieListItem: typeof import('./src/components/cinema/movieListItem.vue')['default']
MoviePush: typeof import('./src/components/cinema/MoviePush.vue')['default']
NewUser: typeof import('./src/components/admin/dialogs/newUser.vue')['default']
Open: typeof import('./src/components/icons/Open.vue')['default']
Password: typeof import('./src/components/user/dialogs/password.vue')['default']
Person: typeof import('./src/components/icons/Person.vue')['default']
Play: typeof import('./src/components/icons/Play.vue')['default']

View File

@ -10,6 +10,8 @@ import customHeaders from "@/components/cinema/dialogs/customHeaders.vue";
import customSubtitles from "@/components/cinema/dialogs/customSubtitles.vue";
import { RoomMemberPermission } from "@/types/Room";
import { ArrowRight, FolderOpened } from "@element-plus/icons-vue";
const customHeadersDialog = ref<InstanceType<typeof customHeaders>>();
const customSubtitlesDialog = ref<InstanceType<typeof customSubtitles>>();
@ -44,7 +46,11 @@ const {
clearMovieList,
clearMovieListLoading,
getLiveInfo,
liveInfo
liveInfo,
subPath,
// movieList,
switchDir,
dynamic
} = useMovieApi(roomToken.value);
//
@ -87,6 +93,14 @@ const confirmCancelPlayback = async () => {
<div class="card-title">影片列表{{ room.totalMovies }}</div>
<div class="card-body">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item v-for="item in room.movieList" :key="item.id">
<el-button link @click="switchDir(item.id, item.subPath)">
{{ item.label }}
</el-button>
</el-breadcrumb-item>
</el-breadcrumb>
<el-skeleton v-if="moviesLoading" :rows="1" animated />
<div
v-else
@ -95,7 +109,7 @@ const confirmCancelPlayback = async () => {
class="flex justify-around mb-2 rounded-lg bg-zinc-50 hover:bg-white transition-all dark:bg-zinc-800 hover:dark:bg-neutral-800"
>
<div class="m-auto pl-2">
<input v-model="selectMovies" type="checkbox" :value="item['id']" />
<input v-show="!dynamic" v-model="selectMovies" type="checkbox" :value="item['id']" />
</div>
<div class="overflow-hidden text-ellipsis mr-auto p-2 w-7/12">
<b class="block text-base font-semibold" :title="`ID: ${item.id}`">
@ -131,7 +145,14 @@ const confirmCancelPlayback = async () => {
<small class="truncate">{{ item.base!.url || item.id }}</small>
</div>
<div class="m-auto p-2" v-if="room.currentMovie.id === item.id">
<div
class="m-auto p-2"
v-if="
room.currentMovie.id === item.id &&
!item.base?.isFolder &&
item.subPath === room.currentMovie.subPath
"
>
<button
class="btn btn-dense btn-success border-green-500 text-green-600 bg-green-100 dark:bg-green-950 dark:border-green-800 m-0 mr-5"
disabled
@ -158,15 +179,28 @@ const confirmCancelPlayback = async () => {
<div class="m-auto p-2" v-else>
<button
v-if="can(RoomMemberPermission.PermissionSetCurrentMovie)"
v-if="can(RoomMemberPermission.PermissionSetCurrentMovie) && !item.base?.isFolder"
class="btn btn-dense m-0 mr-1"
@click="changeCurrentMovie(item['id'])"
@click="changeCurrentMovie(item['id'], true, item.subPath)"
>
播放
<PlayIcon class="inline-block" width="18px" />
</button>
<button
v-if="can(RoomMemberPermission.PermissionEditMovie)"
v-if="can(RoomMemberPermission.PermissionSetCurrentMovie) && item.base?.isFolder"
class="btn btn-dense m-0 mr-1"
@click="switchDir(item['id'], item.subPath)"
>
进入
<el-icon width="18px">
<FolderOpened />
</el-icon>
<FolderOpenedIcon class="inline-block" width="18px" />
</button>
<button
v-if="can(RoomMemberPermission.PermissionEditMovie) && !dynamic"
class="btn btn-dense btn-warning m-0 mr-1"
@click="openEditDialog(item)"
>
@ -174,7 +208,7 @@ const confirmCancelPlayback = async () => {
<EditIcon class="inline-block" width="16px" height="16px" />
</button>
<el-popconfirm
v-if="can(RoomMemberPermission.PermissionDeleteMovie)"
v-if="can(RoomMemberPermission.PermissionDeleteMovie) && !dynamic"
width="220"
confirm-button-text="是"
cancel-button-text="否"
@ -223,8 +257,18 @@ const confirmCancelPlayback = async () => {
:pager-count="5"
layout="sizes, prev, pager, next, jumper"
:total="room.totalMovies"
@size-change="getMovies()"
@current-change="getMovies()"
@size-change="
getMovies(
room.movieList[room.movieList.length - 1].id,
room.movieList[room.movieList.length - 1].subPath
)
"
@current-change="
getMovies(
room.movieList[room.movieList.length - 1].id,
room.movieList[room.movieList.length - 1].subPath
)
"
/>
<div></div>
@ -246,18 +290,28 @@ const confirmCancelPlayback = async () => {
</template>
</el-popconfirm>
<el-popconfirm
v-if="can(RoomMemberPermission.PermissionDeleteMovie)"
v-if="can(RoomMemberPermission.PermissionDeleteMovie) && !dynamic"
width="220"
confirm-button-text="是"
cancel-button-text="否"
title="你确定要清空影片列表吗?!"
title="你确定要清空当前目录吗?!"
@confirm="confirmClear"
>
<template #reference>
<button class="btn btn-error mx-2">清空列表</button>
<button class="btn btn-error mx-2">清空当前目录</button>
</template>
</el-popconfirm>
<button class="btn btn-success" @click="getMovies()">更新列表</button>
<button
class="btn btn-success"
@click="
getMovies(
room.movieList[room.movieList.length - 1].id,
room.movieList[room.movieList.length - 1].subPath
)
"
>
更新列表
</button>
</div>
</div>
</div>

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import { ElNotification } from "element-plus";
import type { BaseMovieInfo } from "@/types/Movie";
import { strLengthLimit } from "@/utils";
import { pushMovieApi } from "@/services/apis/movie";
import { roomStore } from "@/stores/room";
import { getVendorBackends as biliBiliBackends } from "@/services/apis/vendor";
import customHeaders from "@/components/cinema/dialogs/customHeaders.vue";
import customSubtitles from "@/components/cinema/dialogs/customSubtitles.vue";
@ -23,6 +24,8 @@ const Props = defineProps<{
token: string;
}>();
const room = roomStore();
//
const newMovieInfo = ref<BaseMovieInfo>({
url: "",
@ -31,11 +34,13 @@ const newMovieInfo = ref<BaseMovieInfo>({
proxy: false,
live: false,
rtmpSource: false,
headers: {}
headers: {},
isFolder: false
});
enum pushType {
MOVIE = 0,
DIR,
LIVE,
PROXY_LIVE,
RTMP_SOURCE,
@ -169,6 +174,17 @@ const movieTypeRecords: Map<pushType, movieTypeRecord> = new Map([
defaultType: "",
allowedTypes: []
}
],
[
pushType.DIR,
{
name: "文件夹",
comment: "新建普通文件夹",
showProxy: false,
defaultType: "",
allowedTypes: [],
isFolder: true
}
]
]);
@ -264,6 +280,18 @@ const selectPushType = () => {
}
};
break;
case pushType.DIR:
newMovieInfo.value = {
url: newMovieInfo.value.url,
name: newMovieInfo.value.name,
type: movieTypeRecords.get(selectedMovieType.value)?.defaultType || "",
proxy: false,
live: false,
rtmpSource: false,
headers: {},
isFolder: true
};
break;
}
newMovieInfo.value.type = movieTypeRecords.get(selectedMovieType.value)?.defaultType || "";
@ -294,7 +322,7 @@ const biliParse = () => {
//
const { execute: reqPushMovieApi } = pushMovieApi();
const pushMovie = async () => {
if (newMovieInfo.value.live) {
if (newMovieInfo.value.live || newMovieInfo.value.isFolder) {
if (newMovieInfo.value.name === "")
return ElNotification({
title: "添加失败",
@ -319,7 +347,10 @@ const pushMovie = async () => {
strLengthLimit(key, 32);
}
await reqPushMovieApi({
data: newMovieInfo.value,
data: {
...newMovieInfo.value,
parentId: room.movieList[room.movieList.length - 1].id
},
headers: { Authorization: Props.token }
});
@ -410,6 +441,16 @@ const getBiliBiliVendors = async () => {
v-model="newMovieInfo.url"
/>
</div>
<div class="w-full" v-if="selectedMovieType === pushType.DIR">
<input
type="text"
placeholder="文件夹名"
class="l-input-slate mb-2 w-full"
v-model="newMovieInfo.name"
/>
</div>
<div class="w-full" v-if="selectedMovieType === pushType.RTMP_SOURCE">
<input
type="text"
@ -437,7 +478,7 @@ const getBiliBiliVendors = async () => {
</div>
</div>
</div>
<div class="mx-5" v-if="!newMovieInfo.vendorInfo?.vendor">
<div class="mx-5" v-if="!newMovieInfo.vendorInfo?.vendor && selectedMovieType != pushType.DIR">
<el-collapse @change="" class="bg-transparent" style="background: #aaa0 !important">
<el-collapse-item>
<template #title><div class="text-base font-medium">高级选项</div></template>

View File

@ -4,6 +4,7 @@ import { ElNotification, ElMessage } from "element-plus";
import type { BaseMovieInfo, BilibiliVideoInfos } from "@/types/Movie";
import { parseBiliBiliVideo } from "@/services/apis/vendor";
import { pushMoviesApi } from "@/services/apis/movie";
import { roomStore } from "@/stores/room";
import { userStore } from "@/stores/user";
const Props = defineProps<{
@ -12,6 +13,8 @@ const Props = defineProps<{
vendor: string;
}>();
const room = roomStore();
//
const { token: userToken } = userStore();
const biliVideos = ref<BilibiliVideoInfos[]>([]);
@ -130,7 +133,8 @@ const submit = async () => {
shared: item.shared
},
backend: Props.vendor
}
},
parentId: room.movieList[room.movieList.length - 1].id
}
)
});

View File

@ -5,6 +5,7 @@ import index from "./index.vue";
import { getAListFileList } from "@/services/apis/vendor";
import { pushMoviesApi } from "@/services/apis/movie";
import { userStore } from "@/stores/user";
import { roomStore } from "@/stores/room";
import type { BaseMovieInfo } from "@/types/Movie";
import type { FileItem } from "@/types/Vendor";
@ -17,6 +18,8 @@ const props = defineProps<{
roomToken: string;
}>();
const room = roomStore();
const FileList = ref<InstanceType<typeof index>>();
const { token: userToken } = userStore();
const open = ref(false);
@ -71,7 +74,9 @@ const submit = async () => {
alist: {
path: item.path
}
}
},
isFolder: item.isDir,
parentId: room.movieList[room.movieList.length - 1].id
}
)
});
@ -121,7 +126,7 @@ defineExpose({
<template #item="{ item }">
<div class="w-4 mr-6 flex items-center">
<input
v-if="!item.isDir && FileList?.findItem(item)"
v-if="FileList?.findItem(item)"
type="checkbox"
@click.stop=""
@change="FileList?.setProxy(item)"

View File

@ -5,6 +5,7 @@ import index from "./index.vue";
import { getEmbyFileList } from "@/services/apis/vendor";
import { pushMoviesApi } from "@/services/apis/movie";
import { userStore } from "@/stores/user";
import { roomStore } from "@/stores/room";
import type { BaseMovieInfo } from "@/types/Movie";
import type { FileItem } from "@/types/Vendor";
@ -16,6 +17,8 @@ const props = defineProps<{
roomToken: string;
}>();
const room = roomStore();
const FileList = ref<InstanceType<typeof index>>();
const { token: userToken } = userStore();
const open = ref(false);
@ -70,7 +73,9 @@ const submit = async () => {
emby: {
path: item.path
}
}
},
isFolder: item.isDir,
parentId: room.movieList[room.movieList.length - 1].id
}
)
});
@ -109,7 +114,7 @@ defineExpose({
<template #item="{ item }">
<div class="w-4 mr-8 flex items-center">
<input
v-if="!item.isDir && FileList?.findItem(item)"
v-if="FileList?.findItem(item)"
type="checkbox"
@click.stop=""
@change="FileList?.setProxy(item)"

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import type { FileList, FileItem } from "@/types/Vendor";
import { ArrowRight, Folder, Document, Search } from "@element-plus/icons-vue";
import { ArrowRight, Folder, Document, Search, Plus } from "@element-plus/icons-vue";
const props = defineProps<{
fileList: FileList | undefined;
@ -46,8 +46,8 @@ const isSelect = (item: FileItem) => {
const currentPage = ref(1);
const pageSize = ref(10);
const selectOrToDir = (item: FileItem) => {
if (item.isDir) {
const selectOrToDir = (item: FileItem, select?: boolean) => {
if (item.isDir && !select) {
toDir(item.path, true, true);
} else {
findItem(item) ? removeItem(item) : selectItem(item);
@ -113,15 +113,25 @@ defineExpose({
"
v-for="(item, i) in fileList?.items"
:key="i"
@click="selectOrToDir(item)"
@click="selectOrToDir(item, true)"
>
<p
<div
class="truncate overflow-hidden mr-auto max-w-[70%] xl:max-w-[40%] 2xl:max-w-[50%] flex items-center"
>
<el-icon v-if="item.isDir" class="mr-2"><Folder /></el-icon>
<el-icon v-else class="mr-2"><Document /></el-icon>
{{ item.name }}
</p>
<div v-if="item.isDir" class="flex items-center">
<el-icon class="mr-2">
<Folder />
</el-icon>
<p class="hover:underline" @click.stop="selectOrToDir(item)">
{{ item.name }}
</p>
</div>
<div v-else class="flex items-center">
<el-icon class="mr-2"><Document /></el-icon>
<p @click.stop="selectOrToDir(item)">{{ item.name }}</p>
</div>
</div>
<slot name="item" :item="item"></slot>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { ref } from "vue";
import { computed, ref } from "vue";
import { ElNotification } from "element-plus";
import { roomStore } from "@/stores/room";
import {
@ -21,15 +21,47 @@ export const useMovieApi = (roomToken: string) => {
// 获取影片列表和正在播放的影片
const currentPage = ref(1);
const pageSize = ref(10);
const dynamic = ref(false);
const subPath = computed(() => {
return room.movieList && room.movieList[room.movieList.length - 1].subPath;
});
const switchDir = (id: string, subPath: string) => {
if (!room.movieList) return;
const file = room.movieList
.filter((item) => item.id === id)
.find((item) => item.subPath === subPath);
if (!file) {
const movie = room.movies
.filter((movie) => movie.id === id)
.find((movie) => movie.subPath === subPath);
room.movieList.push({
label: movie?.base.name || "",
subPath: movie?.subPath || "",
id: movie?.id || ""
});
} else {
room.movieList = room.movieList.slice(0, room.movieList.indexOf(file) + 1);
}
currentPage.value = 1;
return getMovies(id, subPath);
};
// 获取影片列表
const { state: movies, isLoading: moviesLoading, execute: reqMoviesApi } = moviesApi();
const getMovies = async () => {
const getMovies = async (id?: string, subPath?: string) => {
id = id || room.movieList[room.movieList.length - 1].id;
subPath = subPath || room.movieList[room.movieList.length - 1].subPath;
try {
await reqMoviesApi({
params: {
page: currentPage.value,
max: pageSize.value
max: pageSize.value,
subPath,
id
},
headers: { Authorization: roomToken }
});
@ -38,6 +70,7 @@ export const useMovieApi = (roomToken: string) => {
room.movies = movies.value.movies;
room.totalMovies = movies.value.total;
}
dynamic.value = movies.value?.dynamic || false;
} catch (err: any) {
console.log(err);
ElNotification({
@ -122,11 +155,12 @@ export const useMovieApi = (roomToken: string) => {
// 设置当前正在播放的影片
const { execute: reqChangeCurrentMovieApi, isLoading: changeCurrentMovieLoading } =
changeCurrentMovieApi();
const changeCurrentMovie = async (id: string, showMsg = true) => {
const changeCurrentMovie = async (id: string, showMsg = true, subPath = "") => {
try {
await reqChangeCurrentMovieApi({
data: {
id: id
id: id,
subPath: subPath
},
headers: { Authorization: roomToken }
});
@ -242,9 +276,12 @@ export const useMovieApi = (roomToken: string) => {
const clearMovieList = async () => {
try {
await reqClearMovieListApi({
headers: { Authorization: roomToken }
headers: { Authorization: roomToken },
data: {
parentId: room.movieList[room.movieList.length - 1].id
}
});
await changeCurrentMovie("", false);
ElNotification({
title: "已清空",
type: "success"
@ -282,6 +319,8 @@ export const useMovieApi = (roomToken: string) => {
return {
currentPage,
pageSize,
subPath,
// movieList,
getMovies,
movies,
@ -309,6 +348,9 @@ export const useMovieApi = (roomToken: string) => {
clearMovieListLoading,
getLiveInfo,
liveInfo
liveInfo,
switchDir,
dynamic
};
};

View File

@ -30,12 +30,15 @@ export const moviesApi = useDefineApi<
params: {
page: number;
max: number;
subPath?: string;
id?: string;
};
headers: { Authorization: string };
},
{
movies: MovieInfo[] | [];
total: number;
dynamic: boolean;
}
>({
url: "/api/movie/movies",
@ -128,6 +131,7 @@ export const changeCurrentMovieApi = useDefineApi<
headers: { Authorization: string };
data: {
id: string;
subPath?: string;
};
},
{}
@ -140,6 +144,9 @@ export const changeCurrentMovieApi = useDefineApi<
export const clearMovieListApi = useDefineApi<
{
headers: { Authorization: string };
data?: {
parentId: string;
};
},
{}
>({

View File

@ -1,6 +1,8 @@
import { ref, computed, reactive } from "vue";
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import { useRouteParams } from "@vueuse/router";
import { useLocalStorage } from "@vueuse/core";
import type { CurrentMovie, MovieInfo } from "@/types/Movie";
import { userStore } from "@/stores/user";
import type { MovieStatus } from "@/proto/message";
@ -18,6 +20,15 @@ export const roomStore = defineStore("roomStore", () => {
// 影片列表
const movies = ref<MovieInfo[]>([]);
// roomID
const roomID = computed(() => {
return useRouteParams<string>("roomId");
});
const roomToken = computed(() => {
return useLocalStorage<string>(`room-${roomID.value}-token`, "");
});
const totalMovies = ref(0);
const currentMovie = ref<MovieInfo>({
@ -32,7 +43,8 @@ export const roomStore = defineStore("roomStore", () => {
headers: {}
},
createdAt: 0,
creator: ""
creator: "",
subPath: ""
});
const currentStatus = ref<MovieStatus>({
playing: false,
@ -47,6 +59,20 @@ export const roomStore = defineStore("roomStore", () => {
// 在线人数
const peopleNum = ref(1);
const movieList = ref<
{
label: string;
subPath: string;
id: string;
}[]
>([
{
label: "根目录",
subPath: "",
id: ""
}
]);
return {
isDarkMode,
movies,
@ -57,6 +83,9 @@ export const roomStore = defineStore("roomStore", () => {
play,
peopleNum,
login,
myInfo
myInfo,
roomID,
roomToken,
movieList
};
});

View File

@ -5,6 +5,7 @@ export interface MovieInfo {
base: BaseMovieInfo;
createdAt: number;
creator: string;
subPath: string;
}
export interface CurrentMovie {
@ -35,6 +36,8 @@ export interface BaseMovieInfo {
};
vendorInfo?: VendorInfo;
subtitles?: Subtitles;
isFolder?: boolean;
parentId?: string;
}
export interface EditMovieInfo extends BaseMovieInfo {

View File

@ -322,7 +322,10 @@ const handleElementMessage = (msg: ElementMessage) => {
//
case ElementMessageType.MOVIES_CHANGED: {
getMovies();
getMovies(
room.movieList[room.movieList.length - 1].id,
room.movieList[room.movieList.length - 1].subPath
);
break;
}
@ -485,7 +488,15 @@ onMounted(async () => {
:xs="24"
class="mb-5 max-sm:mb-2"
>
<MoviePush @getMovies="getMovies()" :token="roomToken" />
<MoviePush
@getMovies="
getMovies(
room.movieList[room.movieList.length - 1].id,
room.movieList[room.movieList.length - 1].subPath
)
"
:token="roomToken"
/>
</el-col>
</el-row>
</template>