mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-07-10 23:13:24 +08:00
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -10,12 +10,14 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ColorfulTag: typeof import('./src/components/ColorfulTag.vue')['default']
|
||||
FloatLabelInput: typeof import('./src/components/FloatLabelInput.vue')['default']
|
||||
IconButton: typeof import('./src/components/IconButton.vue')['default']
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||
NDialog: typeof import('naive-ui')['NDialog']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
@@ -31,6 +33,7 @@ declare module 'vue' {
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"pinia": "^3.0.3",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vue-draggable-plus": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.17(typescript@5.6.3)
|
||||
vue-draggable-plus:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0(@types/sortablejs@1.15.8)
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.30.1
|
||||
@@ -738,6 +741,9 @@ packages:
|
||||
'@types/lodash@4.17.20':
|
||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
||||
|
||||
'@types/sortablejs@1.15.8':
|
||||
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.36.0':
|
||||
resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1963,6 +1969,15 @@ packages:
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
vue-draggable-plus@0.6.0:
|
||||
resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==}
|
||||
peerDependencies:
|
||||
'@types/sortablejs': ^1.15.0
|
||||
'@vue/composition-api': '*'
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue-eslint-parser@10.2.0:
|
||||
resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -2576,6 +2591,8 @@ snapshots:
|
||||
|
||||
'@types/lodash@4.17.20': {}
|
||||
|
||||
'@types/sortablejs@1.15.8': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.6.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
@@ -3966,6 +3983,10 @@ snapshots:
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8):
|
||||
dependencies:
|
||||
'@types/sortablejs': 1.15.8
|
||||
|
||||
vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2)):
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -304,6 +304,7 @@ dependencies = [
|
||||
"reqwest-retry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"strfmt",
|
||||
|
||||
@@ -25,6 +25,7 @@ tauri-plugin-dialog = "2"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_repr = "0.1"
|
||||
|
||||
specta = { version = "=2.0.0-rc.20", features = ["serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.20", features = ["derive", "typescript"] }
|
||||
|
||||
@@ -26,11 +26,13 @@ use crate::{
|
||||
cheese_media_url::CheeseMediaUrl, fav_folders::FavFolders, fav_info::FavInfo,
|
||||
get_bangumi_follow_info_params::GetBangumiFollowInfoParams,
|
||||
get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams,
|
||||
get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams,
|
||||
get_user_video_info_params::GetUserVideoInfoParams, normal_info::NormalInfo,
|
||||
normal_media_url::NormalMediaUrl, player_info::PlayerInfo, qrcode_data::QrcodeData,
|
||||
qrcode_status::QrcodeStatus, skip_segments::SkipSegments, subtitle::Subtitle, tags::Tags,
|
||||
user_info::UserInfo, user_video_info::UserVideoInfo, watch_later_info::WatchLaterInfo,
|
||||
get_fav_info_params::GetFavInfoParams, get_history_info_params::GetHistoryInfoParams,
|
||||
get_normal_info_params::GetNormalInfoParams,
|
||||
get_user_video_info_params::GetUserVideoInfoParams, history_info::HistoryInfo,
|
||||
normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo,
|
||||
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, skip_segments::SkipSegments,
|
||||
subtitle::Subtitle, tags::Tags, user_info::UserInfo, user_video_info::UserVideoInfo,
|
||||
watch_later_info::WatchLaterInfo,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -672,6 +674,53 @@ impl BiliClient {
|
||||
Ok(bangumi_follow_info)
|
||||
}
|
||||
|
||||
pub async fn get_history_info(
|
||||
&self,
|
||||
params: GetHistoryInfoParams,
|
||||
) -> anyhow::Result<HistoryInfo> {
|
||||
let device_type: i64 = params.device_type.into();
|
||||
let params = json!({
|
||||
"pn": params.pn,
|
||||
"keyword": params.keyword,
|
||||
"business": "archive",
|
||||
"add_time_start": params.add_time_start,
|
||||
"add_time_end": params.add_time_end,
|
||||
"arc_max_duration": params.arc_max_duration,
|
||||
"arc_min_duration": params.arc_min_duration,
|
||||
"device_type": device_type,
|
||||
});
|
||||
let request = self
|
||||
.api_client
|
||||
.read()
|
||||
.get("https://api.bilibili.com/x/web-interface/history/search")
|
||||
.query(¶ms)
|
||||
.header("cookie", self.get_cookie());
|
||||
let http_resp = request.send().await?;
|
||||
// 检查http响应状态码
|
||||
let status = http_resp.status();
|
||||
let body = http_resp.text().await?;
|
||||
if status != StatusCode::OK {
|
||||
return Err(anyhow!("预料之外的状态码({status}): {body}"));
|
||||
}
|
||||
// 尝试将body解析为BiliResp
|
||||
let bili_resp: BiliResp =
|
||||
serde_json::from_str(&body).context(format!("将body解析为BiliResp失败: {body}"))?;
|
||||
// 检查BiliResp的code字段
|
||||
if bili_resp.code != 0 {
|
||||
return Err(anyhow!("预料之外的code: {bili_resp:?}"));
|
||||
}
|
||||
// 检查BiliResp的data是否存在
|
||||
let Some(data) = bili_resp.data else {
|
||||
return Err(anyhow!("BiliResp中不存在data字段: {bili_resp:?}"));
|
||||
};
|
||||
// 尝试将data解析为HistoryInfo
|
||||
let data_str = data.to_string();
|
||||
let history_info: HistoryInfo = serde_json::from_str(&data_str)
|
||||
.context(format!("将data解析为HistoryInfo失败: {data_str}"))?;
|
||||
|
||||
Ok(history_info)
|
||||
}
|
||||
|
||||
pub async fn get_media_chunk(
|
||||
&self,
|
||||
media_url: &str,
|
||||
|
||||
@@ -18,8 +18,10 @@ use crate::{
|
||||
get_bangumi_info_params::GetBangumiInfoParams,
|
||||
get_cheese_info_params::GetCheeseInfoParams,
|
||||
get_fav_info_params::GetFavInfoParams,
|
||||
get_history_info_params::GetHistoryInfoParams,
|
||||
get_normal_info_params::GetNormalInfoParams,
|
||||
get_user_video_info_params::GetUserVideoInfoParams,
|
||||
history_info::HistoryInfo,
|
||||
normal_info::NormalInfo,
|
||||
qrcode_data::QrcodeData,
|
||||
qrcode_status::QrcodeStatus,
|
||||
@@ -210,6 +212,20 @@ pub async fn get_bangumi_follow_info(
|
||||
Ok(bangumi_follow_info)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
pub async fn get_history_info(
|
||||
app: AppHandle,
|
||||
params: GetHistoryInfoParams,
|
||||
) -> CommandResult<HistoryInfo> {
|
||||
let bili_client = app.get_bili_client();
|
||||
let history_info = bili_client
|
||||
.get_history_info(params)
|
||||
.await
|
||||
.map_err(|err| CommandError::from("获取历史记录失败", err))?;
|
||||
Ok(history_info)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::danmaku_xml_to_ass::canvas::CanvasConfig;
|
||||
use crate::{
|
||||
danmaku_xml_to_ass::canvas::CanvasConfig,
|
||||
types::{audio_quality::AudioQuality, codec_type::CodecType, video_quality::VideoQuality},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
@@ -14,9 +16,9 @@ pub struct Config {
|
||||
pub download_dir: PathBuf,
|
||||
pub enable_file_logger: bool,
|
||||
pub sessdata: String,
|
||||
pub prefer_video_quality: PreferVideoQuality,
|
||||
pub prefer_codec_type: PreferCodecType,
|
||||
pub prefer_audio_quality: PreferAudioQuality,
|
||||
pub video_quality_priority: Vec<VideoQuality>,
|
||||
pub codec_type_priority: Vec<CodecType>,
|
||||
pub audio_quality_priority: Vec<AudioQuality>,
|
||||
pub download_video: bool,
|
||||
pub download_audio: bool,
|
||||
pub auto_merge: bool,
|
||||
@@ -96,13 +98,36 @@ impl Config {
|
||||
fn default(app_data_dir: &Path) -> Config {
|
||||
const DEFAULT_FMT_FOR_PART: &str =
|
||||
"{collection_title}/{episode_title}/{episode_title}-P{part_order} {part_title}";
|
||||
let default_video_quality_priority = vec![
|
||||
VideoQuality::Video8K,
|
||||
VideoQuality::VideoDolby,
|
||||
VideoQuality::VideoHDR,
|
||||
VideoQuality::Video4K,
|
||||
VideoQuality::Video1080P60,
|
||||
VideoQuality::Video1080PPlus,
|
||||
VideoQuality::Video1080P,
|
||||
VideoQuality::VideoAiRepair,
|
||||
VideoQuality::Video720P60,
|
||||
VideoQuality::Video720P,
|
||||
VideoQuality::Video480P,
|
||||
VideoQuality::Video360P,
|
||||
VideoQuality::Video240P,
|
||||
];
|
||||
let default_audio_quality_priority = vec![
|
||||
AudioQuality::AudioHiRes,
|
||||
AudioQuality::AudioDolby,
|
||||
AudioQuality::Audio192K,
|
||||
AudioQuality::Audio132K,
|
||||
AudioQuality::Audio64K,
|
||||
];
|
||||
|
||||
Config {
|
||||
download_dir: app_data_dir.join("视频下载"),
|
||||
enable_file_logger: true,
|
||||
sessdata: String::new(),
|
||||
prefer_video_quality: PreferVideoQuality::Best,
|
||||
prefer_codec_type: PreferCodecType::AVC,
|
||||
prefer_audio_quality: PreferAudioQuality::Best,
|
||||
video_quality_priority: default_video_quality_priority,
|
||||
codec_type_priority: vec![CodecType::AVC, CodecType::HEVC, CodecType::AV1],
|
||||
audio_quality_priority: default_audio_quality_priority,
|
||||
download_video: true,
|
||||
download_audio: true,
|
||||
auto_merge: true,
|
||||
@@ -130,102 +155,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Type,
|
||||
IntoPrimitive,
|
||||
FromPrimitive,
|
||||
)]
|
||||
#[repr(i64)]
|
||||
pub enum PreferVideoQuality {
|
||||
#[default]
|
||||
Best = -1,
|
||||
|
||||
#[serde(rename = "240P")]
|
||||
Video240P = 6,
|
||||
#[serde(rename = "360P")]
|
||||
Video360P = 16,
|
||||
#[serde(rename = "480P")]
|
||||
Video480P = 32,
|
||||
#[serde(rename = "720P")]
|
||||
Video720P = 64,
|
||||
#[serde(rename = "720P60")]
|
||||
Video720P60 = 74,
|
||||
#[serde(rename = "1080P")]
|
||||
Video1080P = 80,
|
||||
#[serde(rename = "AiRepair")]
|
||||
VideoAiRepair = 100,
|
||||
#[serde(rename = "1080P+")]
|
||||
Video1080PPlus = 112,
|
||||
#[serde(rename = "1080P60")]
|
||||
Video1080P60 = 116,
|
||||
#[serde(rename = "4K")]
|
||||
Video4K = 120,
|
||||
#[serde(rename = "HDR")]
|
||||
VideoHDR = 125,
|
||||
#[serde(rename = "Dolby")]
|
||||
VideoDolby = 126,
|
||||
#[serde(rename = "8K")]
|
||||
Video8K = 127,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Type,
|
||||
IntoPrimitive,
|
||||
FromPrimitive,
|
||||
)]
|
||||
#[repr(i64)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum PreferCodecType {
|
||||
#[default]
|
||||
Unknown = -1,
|
||||
|
||||
AVC = 7,
|
||||
HEVC = 12,
|
||||
AV1 = 13,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Type,
|
||||
IntoPrimitive,
|
||||
FromPrimitive,
|
||||
)]
|
||||
#[repr(i64)]
|
||||
pub enum PreferAudioQuality {
|
||||
#[default]
|
||||
Best = -1,
|
||||
#[serde(rename = "64K")]
|
||||
Audio64K = 30216,
|
||||
#[serde(rename = "132K")]
|
||||
Audio132K = 30232,
|
||||
#[serde(rename = "192K")]
|
||||
Audio192K = 30280,
|
||||
#[serde(rename = "Dolby")]
|
||||
AudioDolby = 30250,
|
||||
#[serde(rename = "HiRes")]
|
||||
AudioHiRes = 30251,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub enum ProxyMode {
|
||||
#[default]
|
||||
|
||||
@@ -170,7 +170,7 @@ impl DownloadTask {
|
||||
|
||||
let download_task = async {
|
||||
download_task_option
|
||||
.get_or_insert(Box::pin(self.download()))
|
||||
.get_or_insert_with(|| Box::pin(self.download()))
|
||||
.await;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
fs::{File, OpenOptions},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -114,11 +114,7 @@ impl AudioTask {
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
self.prepare(app, medias)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -174,11 +170,7 @@ impl AudioTask {
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
self.prepare(app, medias)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -234,37 +226,28 @@ impl AudioTask {
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
self.prepare(app, medias)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) {
|
||||
medias.sort_by_key(|m| Reverse(m.id.to_audio_quality_for_prepare()));
|
||||
let best_quality_id = medias[0].id;
|
||||
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) -> anyhow::Result<()> {
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
let prefer_quality = app.get_config().read().prefer_audio_quality;
|
||||
let prefer_quality_id: i64 = prefer_quality.into();
|
||||
let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id);
|
||||
let quality_filtered_medias: Vec<MediaForPrepare> = if prefer_quality_found {
|
||||
// 如果用户指定质量存在,则使用用户指定的质量
|
||||
medias
|
||||
.into_iter()
|
||||
.filter(|m| m.id == prefer_quality_id)
|
||||
.collect()
|
||||
} else {
|
||||
// 否则使用最高质量
|
||||
medias
|
||||
.into_iter()
|
||||
.filter(|m| m.id == best_quality_id)
|
||||
.collect()
|
||||
};
|
||||
let quality_priority = app.get_config().read().audio_quality_priority.clone();
|
||||
let priority_map: HashMap<&AudioQuality, usize> = quality_priority
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, quality)| (quality, index))
|
||||
.collect();
|
||||
medias.sort_by_key(|media| {
|
||||
let quality: AudioQuality = media.id.into();
|
||||
priority_map.get(&quality).unwrap_or(&usize::MAX)
|
||||
});
|
||||
|
||||
let media = &quality_filtered_medias[0];
|
||||
let media = &medias[0];
|
||||
|
||||
self.audio_quality = media.id.into();
|
||||
|
||||
@@ -295,6 +278,8 @@ impl AudioTask {
|
||||
self.content_length = content_length;
|
||||
self.chunks = chunks;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_uncompleted(&mut self) {
|
||||
@@ -427,30 +412,3 @@ struct MediaForPrepare {
|
||||
pub id: i64,
|
||||
pub url_with_content_length: Vec<(String, u64)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum AudioQualityForPrepare {
|
||||
Audio64K,
|
||||
Audio132K,
|
||||
Audio192K,
|
||||
AudioDolby,
|
||||
AudioHiRes,
|
||||
}
|
||||
|
||||
trait ToAudioQualityForPrepare {
|
||||
fn to_audio_quality_for_prepare(self) -> Option<AudioQualityForPrepare>;
|
||||
}
|
||||
|
||||
impl ToAudioQualityForPrepare for i64 {
|
||||
fn to_audio_quality_for_prepare(self) -> Option<AudioQualityForPrepare> {
|
||||
let audio_quality: AudioQuality = self.into();
|
||||
match audio_quality {
|
||||
AudioQuality::Audio64K => Some(AudioQualityForPrepare::Audio64K),
|
||||
AudioQuality::Audio132K => Some(AudioQualityForPrepare::Audio132K),
|
||||
AudioQuality::Audio192K => Some(AudioQualityForPrepare::Audio192K),
|
||||
AudioQuality::AudioDolby => Some(AudioQualityForPrepare::AudioDolby),
|
||||
AudioQuality::AudioHiRes => Some(AudioQualityForPrepare::AudioHiRes),
|
||||
AudioQuality::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,27 +98,16 @@ impl VideoProcessTask {
|
||||
tokio::spawn(async move {
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
|
||||
command
|
||||
.arg("-i")
|
||||
.arg(video_path_clone)
|
||||
.arg("-i")
|
||||
.arg(audio_path_clone);
|
||||
|
||||
command.arg("-i").arg(video_path_clone);
|
||||
command.arg("-i").arg(audio_path_clone);
|
||||
if let Some(metadata_path) = metadata_path_clone {
|
||||
command
|
||||
.arg("-i")
|
||||
.arg(metadata_path)
|
||||
.arg("-map_metadata")
|
||||
.arg("2");
|
||||
command.arg("-i").arg(metadata_path);
|
||||
command.arg("-map_metadata").arg("2");
|
||||
}
|
||||
|
||||
command
|
||||
.arg("-c")
|
||||
.arg("copy")
|
||||
.arg("-map")
|
||||
.arg("0:v:0")
|
||||
.arg("-map")
|
||||
.arg("1:a:0");
|
||||
command.arg("-c").arg("copy");
|
||||
command.arg("-map").arg("0:v:0");
|
||||
command.arg("-map").arg("1:a:0");
|
||||
|
||||
command.arg(output_path_clone).arg("-y");
|
||||
|
||||
@@ -198,19 +187,14 @@ impl VideoProcessTask {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
|
||||
command
|
||||
.arg("-i")
|
||||
.arg(video_path_clone)
|
||||
.arg("-i")
|
||||
.arg(audio_path_clone)
|
||||
.arg("-c")
|
||||
.arg("copy")
|
||||
.arg("-map")
|
||||
.arg("0:v:0")
|
||||
.arg("-map")
|
||||
.arg("1:a:0")
|
||||
.arg(output_path_clone)
|
||||
.arg("-y");
|
||||
command.arg("-i").arg(video_path_clone);
|
||||
command.arg("-i").arg(audio_path_clone);
|
||||
|
||||
command.arg("-c").arg("copy");
|
||||
command.arg("-map").arg("0:v:0");
|
||||
command.arg("-map").arg("1:a:0");
|
||||
|
||||
command.arg(output_path_clone).arg("-y");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -286,17 +270,13 @@ impl VideoProcessTask {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let mut command = std::process::Command::new(ffmpeg_program);
|
||||
|
||||
command
|
||||
.arg("-i")
|
||||
.arg(video_path_clone)
|
||||
.arg("-i")
|
||||
.arg(metadata_path_clone)
|
||||
.arg("-map_metadata")
|
||||
.arg("1")
|
||||
.arg("-c")
|
||||
.arg("copy")
|
||||
.arg(output_path_clone)
|
||||
.arg("-y");
|
||||
command.arg("-i").arg(video_path_clone);
|
||||
command.arg("-i").arg(metadata_path_clone);
|
||||
|
||||
command.arg("-map_metadata").arg("1");
|
||||
command.arg("-c").arg("copy");
|
||||
|
||||
command.arg(output_path_clone).arg("-y");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
fs::{File, OpenOptions},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -74,11 +74,7 @@ impl VideoTask {
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取视频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
self.prepare(app, medias)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -144,11 +140,7 @@ impl VideoTask {
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取视频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
self.prepare(app, medias)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -214,47 +206,49 @@ impl VideoTask {
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取视频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
self.prepare(app, medias)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) {
|
||||
medias.sort_by_key(|m| Reverse(m.id));
|
||||
let best_quality_id = medias[0].id;
|
||||
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) -> anyhow::Result<()> {
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
let (prefer_quality, prefer_codec_type) = {
|
||||
let (video_quality_priority, codec_type_priority) = {
|
||||
let config = app.get_config().inner().read();
|
||||
(config.prefer_video_quality, config.prefer_codec_type)
|
||||
(
|
||||
config.video_quality_priority.clone(),
|
||||
config.codec_type_priority.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let prefer_quality_id: i64 = prefer_quality.into();
|
||||
let prefer_codec_id: i64 = prefer_codec_type.into();
|
||||
let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id);
|
||||
let mut quality_filtered_medias: Vec<MediaForPrepare> = if prefer_quality_found {
|
||||
// 如果用户指定质量存在,则使用用户指定的质量
|
||||
medias
|
||||
.into_iter()
|
||||
.filter(|m| m.id == prefer_quality_id)
|
||||
.collect()
|
||||
} else {
|
||||
// 否则使用最高质量
|
||||
medias
|
||||
.into_iter()
|
||||
.filter(|m| m.id == best_quality_id)
|
||||
.collect()
|
||||
};
|
||||
// 按照 AVC > HEVC > AV1 的顺序排列
|
||||
quality_filtered_medias.sort_by_key(|m| m.codecid);
|
||||
|
||||
let media = quality_filtered_medias
|
||||
let video_priority_map: HashMap<&VideoQuality, usize> = video_quality_priority
|
||||
.iter()
|
||||
.find(|m| m.codecid == prefer_codec_id)
|
||||
.unwrap_or(&quality_filtered_medias[0]);
|
||||
.enumerate()
|
||||
.map(|(index, quality)| (quality, index))
|
||||
.collect();
|
||||
medias.sort_by_key(|media| {
|
||||
let quality: VideoQuality = media.id.into();
|
||||
video_priority_map.get(&quality).unwrap_or(&usize::MAX)
|
||||
});
|
||||
|
||||
let retain_id = medias[0].id;
|
||||
medias.retain(|m| m.id == retain_id);
|
||||
|
||||
let codec_priority_map: HashMap<&CodecType, usize> = codec_type_priority
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, codec_type)| (codec_type, index))
|
||||
.collect();
|
||||
|
||||
medias.sort_by_key(|m| {
|
||||
let codec_type: CodecType = m.codecid.into();
|
||||
codec_priority_map.get(&codec_type).unwrap_or(&usize::MAX)
|
||||
});
|
||||
|
||||
let media = &medias[0];
|
||||
|
||||
self.video_quality = media.id.into();
|
||||
self.codec_type = media.codecid.into();
|
||||
@@ -286,6 +280,8 @@ impl VideoTask {
|
||||
self.content_length = content_length;
|
||||
self.chunks = chunks;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_uncompleted(&mut self) {
|
||||
|
||||
@@ -46,6 +46,7 @@ pub fn run() {
|
||||
get_fav_info,
|
||||
get_watch_later_info,
|
||||
get_bangumi_follow_info,
|
||||
get_history_info,
|
||||
create_download_tasks,
|
||||
pause_download_tasks,
|
||||
resume_download_tasks,
|
||||
|
||||
@@ -7,6 +7,8 @@ use specta::Type;
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct BangumiFollowInfo {
|
||||
pub list: Vec<EpInBangumiFollow>,
|
||||
pub pn: i64,
|
||||
@@ -10,6 +11,7 @@ pub struct BangumiFollowInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct EpInBangumiFollow {
|
||||
pub season_id: i64,
|
||||
pub media_id: i64,
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct BangumiInfo {
|
||||
pub activity: Activity,
|
||||
pub actors: String,
|
||||
@@ -88,6 +89,7 @@ impl BangumiInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Activity {
|
||||
pub head_bg_url: String,
|
||||
pub id: i64,
|
||||
@@ -95,12 +97,14 @@ pub struct Activity {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct AreaInBangumi {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct EpInBangumi {
|
||||
pub aid: i64,
|
||||
@@ -140,6 +144,7 @@ pub struct EpInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct BadgeInfoInBangumi {
|
||||
pub bg_color: String,
|
||||
pub bg_color_night: String,
|
||||
@@ -147,6 +152,7 @@ pub struct BadgeInfoInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DimensionInBangumi {
|
||||
pub height: i64,
|
||||
pub rotate: i64,
|
||||
@@ -154,6 +160,7 @@ pub struct DimensionInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RightsInBangumiEp {
|
||||
pub allow_dm: i64,
|
||||
pub allow_download: i64,
|
||||
@@ -161,30 +168,35 @@ pub struct RightsInBangumiEp {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Skip {
|
||||
pub ed: Ed,
|
||||
pub op: Op,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Ed {
|
||||
pub end: i64,
|
||||
pub start: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Op {
|
||||
pub end: i64,
|
||||
pub start: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct IconFont {
|
||||
pub name: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct NewEp {
|
||||
pub desc: String,
|
||||
pub id: i64,
|
||||
@@ -193,6 +205,7 @@ pub struct NewEp {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PaymentInBangumi {
|
||||
pub discount: i64,
|
||||
pub pay_type: PayType,
|
||||
@@ -207,6 +220,7 @@ pub struct PaymentInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PayType {
|
||||
pub allow_discount: i64,
|
||||
pub allow_pack: i64,
|
||||
@@ -217,17 +231,20 @@ pub struct PayType {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PlayStrategy {
|
||||
pub strategies: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Positive {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PublishInBangumi {
|
||||
pub is_finish: i64,
|
||||
pub is_started: i64,
|
||||
@@ -238,12 +255,14 @@ pub struct PublishInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RatingInBangumi {
|
||||
pub count: i64,
|
||||
pub score: f64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RightsInBangumi {
|
||||
pub allow_bp: i64,
|
||||
pub allow_bp_rank: i64,
|
||||
@@ -263,6 +282,7 @@ pub struct RightsInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Season {
|
||||
pub badge: String,
|
||||
@@ -282,6 +302,7 @@ pub struct Season {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct NewEpInSeason {
|
||||
pub cover: String,
|
||||
pub id: i64,
|
||||
@@ -289,6 +310,7 @@ pub struct NewEpInSeason {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInSeason {
|
||||
pub favorites: i64,
|
||||
pub series_follow: i64,
|
||||
@@ -297,6 +319,7 @@ pub struct StatInSeason {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct SeriesInBangumi {
|
||||
pub display_type: i64,
|
||||
@@ -305,11 +328,13 @@ pub struct SeriesInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Show {
|
||||
pub wide_screen: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInBangumi {
|
||||
pub coins: i64,
|
||||
pub danmakus: i64,
|
||||
@@ -324,6 +349,7 @@ pub struct StatInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UpInfoInBangumi {
|
||||
pub avatar: String,
|
||||
pub mid: i64,
|
||||
@@ -331,6 +357,7 @@ pub struct UpInfoInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserStatusInBangumi {
|
||||
pub area_limit: i64,
|
||||
pub ban_area_show: i64,
|
||||
@@ -343,6 +370,7 @@ pub struct UserStatusInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SectionInBangumi {
|
||||
pub attr: i64,
|
||||
pub episodes: Vec<EpInBangumi>,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct BangumiMediaUrl {
|
||||
pub accept_format: String,
|
||||
pub code: i64,
|
||||
@@ -38,12 +39,14 @@ pub struct BangumiMediaUrl {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RecordInfo {
|
||||
pub record_icon: String,
|
||||
pub record: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SupportFormatInBangumi {
|
||||
pub display_desc: String,
|
||||
pub has_preview: bool,
|
||||
@@ -60,6 +63,7 @@ pub struct SupportFormatInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DashInBangumi {
|
||||
pub duration: u64,
|
||||
pub min_buffer_time: f64,
|
||||
@@ -68,6 +72,7 @@ pub struct DashInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct MediaInBangumi {
|
||||
pub start_with_sap: i64,
|
||||
pub bandwidth: i64,
|
||||
@@ -87,12 +92,14 @@ pub struct MediaInBangumi {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SegmentBaseInBangumi {
|
||||
pub initialization: String,
|
||||
pub index_range: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct ClipInfoList {
|
||||
#[serde(rename = "materialNo")]
|
||||
pub material_no: i64,
|
||||
@@ -105,12 +112,14 @@ pub struct ClipInfoList {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DurlInBangumi {
|
||||
pub durl: Vec<DurlDetailInBangumi>,
|
||||
pub quality: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DurlDetailInBangumi {
|
||||
pub size: i64,
|
||||
pub ahead: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct CheeseInfo {
|
||||
pub abtest_info: AbtestInfo,
|
||||
@@ -54,11 +55,13 @@ pub struct CheeseInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct AbtestInfo {
|
||||
pub style_abtest: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Brief {
|
||||
pub content: String,
|
||||
pub img: Vec<Img>,
|
||||
@@ -68,23 +71,27 @@ pub struct Brief {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Img {
|
||||
pub aspect_ratio: f64,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Consulting {
|
||||
pub consulting_flag: bool,
|
||||
pub consulting_url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Cooperation {
|
||||
pub link: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct EpPage {
|
||||
pub next: bool,
|
||||
pub num: i64,
|
||||
@@ -93,6 +100,7 @@ pub struct EpPage {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct EpTag {
|
||||
pub part_preview_tag: String,
|
||||
@@ -101,6 +109,7 @@ pub struct EpTag {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct EpInCheese {
|
||||
@@ -130,6 +139,7 @@ pub struct EpInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Faq {
|
||||
pub content: String,
|
||||
pub link: String,
|
||||
@@ -137,24 +147,28 @@ pub struct Faq {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Faq1 {
|
||||
pub items: Vec<Faq1Item>,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Faq1Item {
|
||||
pub answer: String,
|
||||
pub question: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PaidJump {
|
||||
pub jump_url_for_app: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Payment {
|
||||
pub bp_enough: i64,
|
||||
pub desc: String,
|
||||
@@ -168,6 +182,7 @@ pub struct Payment {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PreviewedPurchaseNote {
|
||||
pub long_watch_text: String,
|
||||
pub pay_text: String,
|
||||
@@ -177,6 +192,7 @@ pub struct PreviewedPurchaseNote {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PurchaseFormatNote {
|
||||
pub content_list: Vec<ContentList>,
|
||||
pub link: String,
|
||||
@@ -184,6 +200,7 @@ pub struct PurchaseFormatNote {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct ContentList {
|
||||
pub bold: bool,
|
||||
pub content: String,
|
||||
@@ -191,6 +208,7 @@ pub struct ContentList {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PurchaseNote {
|
||||
pub content: String,
|
||||
pub link: String,
|
||||
@@ -198,12 +216,14 @@ pub struct PurchaseNote {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PurchaseProtocol {
|
||||
pub link: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RecommendSeason {
|
||||
pub cover: String,
|
||||
pub ep_count: String,
|
||||
@@ -215,6 +235,7 @@ pub struct RecommendSeason {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInCheese {
|
||||
pub play: i64,
|
||||
pub play_desc: String,
|
||||
@@ -222,6 +243,7 @@ pub struct StatInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UpInfoInCheese {
|
||||
pub avatar: String,
|
||||
pub brief: String,
|
||||
@@ -236,6 +258,7 @@ pub struct UpInfoInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PendantInCheese {
|
||||
pub image: String,
|
||||
pub name: String,
|
||||
@@ -243,6 +266,7 @@ pub struct PendantInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserStatusInCheese {
|
||||
pub bp: i64,
|
||||
pub expire_at: i64,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CheeseMediaUrl {
|
||||
pub accept_format: String,
|
||||
pub code: i64,
|
||||
@@ -32,16 +33,19 @@ pub struct CheeseMediaUrl {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PlayViewBusinessInfo {
|
||||
pub user_status: UserStatusInCheeseUrl,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserStatusInCheeseUrl {
|
||||
pub watch_progress: WatchProgress,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct WatchProgress {
|
||||
pub current_watch_progress: i64,
|
||||
@@ -51,6 +55,7 @@ pub struct WatchProgress {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SupportFormatInCheese {
|
||||
pub display_desc: String,
|
||||
pub superscript: String,
|
||||
@@ -63,6 +68,7 @@ pub struct SupportFormatInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DashInCheese {
|
||||
pub duration: u64,
|
||||
pub min_buffer_time: f64,
|
||||
@@ -71,6 +77,7 @@ pub struct DashInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct MediaInCheese {
|
||||
pub start_with_sap: i64,
|
||||
pub bandwidth: i64,
|
||||
@@ -90,18 +97,21 @@ pub struct MediaInCheese {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SegmentBaseInCheese {
|
||||
pub initialization: String,
|
||||
pub index_range: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DurlInCheese {
|
||||
pub durl: Vec<DurlDetailInCheese>,
|
||||
pub quality: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DurlDetailInCheese {
|
||||
pub size: i64,
|
||||
pub ahead: String,
|
||||
|
||||
@@ -7,6 +7,8 @@ use specta::Type;
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
@@ -19,6 +21,7 @@ use specta::Type;
|
||||
pub enum CodecType {
|
||||
#[default]
|
||||
Unknown = -1,
|
||||
|
||||
Audio = 0,
|
||||
AVC = 7,
|
||||
HEVC = 12,
|
||||
|
||||
@@ -11,18 +11,21 @@ pub enum CreateDownloadTaskParams {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CreateNormalDownloadTaskParams {
|
||||
pub info: NormalInfo,
|
||||
pub aid_cid_pairs: Vec<(i64, Option<i64>)>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CreateBangumiDownloadTaskParams {
|
||||
pub ep_ids: Vec<i64>,
|
||||
pub info: BangumiInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CreateCheeseDownloadTaskParams {
|
||||
pub ep_ids: Vec<i64>,
|
||||
pub info: CheeseInfo,
|
||||
|
||||
@@ -2,12 +2,14 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct FavFolders {
|
||||
pub count: i64,
|
||||
pub list: Vec<Folder>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Folder {
|
||||
pub id: i64,
|
||||
pub fid: i64,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct FavInfo {
|
||||
pub info: Info,
|
||||
pub medias: Option<Vec<MediaInFav>>,
|
||||
@@ -10,6 +11,7 @@ pub struct FavInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Info {
|
||||
pub id: i64,
|
||||
@@ -34,6 +36,7 @@ pub struct Info {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Upper {
|
||||
pub mid: i64,
|
||||
pub name: String,
|
||||
@@ -44,6 +47,7 @@ pub struct Upper {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CntInfo {
|
||||
pub collect: i64,
|
||||
pub play: i64,
|
||||
@@ -52,6 +56,7 @@ pub struct CntInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct MediaInFav {
|
||||
pub id: i64,
|
||||
@@ -76,6 +81,7 @@ pub struct MediaInFav {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UpperInMedia {
|
||||
pub mid: i64,
|
||||
pub name: String,
|
||||
@@ -84,6 +90,7 @@ pub struct UpperInMedia {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CntInfoInMedia {
|
||||
pub collect: i64,
|
||||
pub play: i64,
|
||||
@@ -95,6 +102,7 @@ pub struct CntInfoInMedia {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Ugc {
|
||||
pub first_cid: i64,
|
||||
}
|
||||
|
||||
38
src-tauri/src/types/get_history_info_params.rs
Normal file
38
src-tauri/src/types/get_history_info_params.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub struct GetHistoryInfoParams {
|
||||
pub pn: i64,
|
||||
pub keyword: String,
|
||||
pub add_time_start: i64,
|
||||
pub add_time_end: i64,
|
||||
pub arc_max_duration: i64,
|
||||
pub arc_min_duration: i64,
|
||||
pub device_type: DeviceType,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Type,
|
||||
IntoPrimitive,
|
||||
FromPrimitive,
|
||||
)]
|
||||
#[repr(i64)]
|
||||
pub enum DeviceType {
|
||||
#[default]
|
||||
All = 0,
|
||||
PC = 1,
|
||||
Mobile = 2,
|
||||
Pad = 3,
|
||||
TV = 4,
|
||||
}
|
||||
56
src-tauri/src/types/history_info.rs
Normal file
56
src-tauri/src/types/history_info.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct HistoryInfo {
|
||||
pub has_more: bool,
|
||||
pub page: PageInHistory,
|
||||
pub list: Option<Vec<HistoryDetail>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PageInHistory {
|
||||
pub pn: i64,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct HistoryDetail {
|
||||
pub title: String,
|
||||
pub long_title: String,
|
||||
pub cover: String,
|
||||
pub uri: String,
|
||||
pub history: History,
|
||||
pub videos: i64,
|
||||
pub author_name: String,
|
||||
pub author_face: String,
|
||||
pub author_mid: i64,
|
||||
pub view_at: i64,
|
||||
pub progress: i64,
|
||||
pub badge: String,
|
||||
pub show_title: String,
|
||||
pub duration: i64,
|
||||
pub total: i64,
|
||||
pub new_desc: String,
|
||||
pub is_finish: i64,
|
||||
pub is_fav: i64,
|
||||
pub kid: i64,
|
||||
pub tag_name: String,
|
||||
pub live_status: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct History {
|
||||
pub oid: i64,
|
||||
pub epid: i64,
|
||||
pub bvid: String,
|
||||
pub page: i64,
|
||||
pub cid: i64,
|
||||
pub part: String,
|
||||
pub business: String,
|
||||
pub dt: i64,
|
||||
}
|
||||
@@ -12,8 +12,10 @@ pub mod get_bangumi_follow_info_params;
|
||||
pub mod get_bangumi_info_params;
|
||||
pub mod get_cheese_info_params;
|
||||
pub mod get_fav_info_params;
|
||||
pub mod get_history_info_params;
|
||||
pub mod get_normal_info_params;
|
||||
pub mod get_user_video_info_params;
|
||||
pub mod history_info;
|
||||
pub mod log_level;
|
||||
pub mod normal_info;
|
||||
pub mod normal_media_url;
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct NormalInfo {
|
||||
pub bvid: String,
|
||||
@@ -52,6 +53,7 @@ pub struct NormalInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DescV2 {
|
||||
pub raw_text: String,
|
||||
#[serde(rename = "type")]
|
||||
@@ -60,6 +62,7 @@ pub struct DescV2 {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Rights {
|
||||
pub bp: i64,
|
||||
pub elec: i64,
|
||||
@@ -82,6 +85,7 @@ pub struct Rights {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct OwnerInNormal {
|
||||
pub mid: i64,
|
||||
pub name: String,
|
||||
@@ -89,6 +93,7 @@ pub struct OwnerInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInNormal {
|
||||
pub aid: i64,
|
||||
pub view: i64,
|
||||
@@ -106,6 +111,7 @@ pub struct StatInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct ArgueInfo {
|
||||
pub argue_msg: String,
|
||||
@@ -114,6 +120,7 @@ pub struct ArgueInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Dimension {
|
||||
pub width: i64,
|
||||
pub height: i64,
|
||||
@@ -121,6 +128,7 @@ pub struct Dimension {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct PageInNormal {
|
||||
pub cid: i64,
|
||||
@@ -135,12 +143,14 @@ pub struct PageInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SubtitleInNormal {
|
||||
pub allow_submit: bool,
|
||||
pub list: Vec<SubtitleDetailInNormal>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SubtitleDetailInNormal {
|
||||
pub id: i64,
|
||||
pub lan: String,
|
||||
@@ -155,16 +165,19 @@ pub struct SubtitleDetailInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserGarb {
|
||||
pub url_image_ani_cut: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct HonorReply {
|
||||
pub honor: Option<Vec<Honor>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Honor {
|
||||
pub aid: i64,
|
||||
#[serde(rename = "type")]
|
||||
@@ -174,6 +187,7 @@ pub struct Honor {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UgcSeason {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
@@ -191,6 +205,7 @@ pub struct UgcSeason {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SectionInNormal {
|
||||
pub season_id: i64,
|
||||
pub id: i64,
|
||||
@@ -201,6 +216,7 @@ pub struct SectionInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct EpInNormal {
|
||||
pub season_id: i64,
|
||||
pub section_id: i64,
|
||||
@@ -216,6 +232,7 @@ pub struct EpInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Arc {
|
||||
pub aid: i64,
|
||||
pub videos: i64,
|
||||
@@ -244,6 +261,7 @@ pub struct Arc {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Author {
|
||||
pub mid: i64,
|
||||
pub name: String,
|
||||
@@ -251,6 +269,7 @@ pub struct Author {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInNormalEp {
|
||||
pub aid: i64,
|
||||
pub view: i64,
|
||||
@@ -270,6 +289,7 @@ pub struct StatInNormalEp {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInNormalSeason {
|
||||
pub season_id: i64,
|
||||
pub view: i64,
|
||||
@@ -286,6 +306,7 @@ pub struct StatInNormalSeason {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PageInNormalEp {
|
||||
pub cid: i64,
|
||||
pub page: i64,
|
||||
@@ -298,6 +319,7 @@ pub struct PageInNormalEp {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RightsInNormalEp {
|
||||
pub bp: i64,
|
||||
pub elec: i64,
|
||||
@@ -315,6 +337,7 @@ pub struct RightsInNormalEp {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Staff {
|
||||
pub mid: i64,
|
||||
pub title: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct NormalMediaUrl {
|
||||
pub from: String,
|
||||
pub result: String,
|
||||
@@ -23,6 +24,7 @@ pub struct NormalMediaUrl {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DashInNormal {
|
||||
pub duration: u64,
|
||||
pub min_buffer_time: f64,
|
||||
@@ -33,12 +35,14 @@ pub struct DashInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Flac {
|
||||
pub display: bool,
|
||||
pub audio: Option<MediaInNormal>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct MediaInNormal {
|
||||
pub id: i64,
|
||||
pub start_with_sap: i64,
|
||||
@@ -56,12 +60,14 @@ pub struct MediaInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SegmentBaseInNormal {
|
||||
pub initialization: String,
|
||||
pub index_range: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Dolby {
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: i64,
|
||||
@@ -69,6 +75,7 @@ pub struct Dolby {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SupportFormatInNormal {
|
||||
pub quality: i64,
|
||||
pub format: String,
|
||||
@@ -79,6 +86,7 @@ pub struct SupportFormatInNormal {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PlayConf {
|
||||
pub is_new_description: bool,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct PlayerInfo {
|
||||
pub aid: i64,
|
||||
@@ -45,6 +46,7 @@ pub struct PlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct IpInfo {
|
||||
pub ip: String,
|
||||
pub zone_ip: String,
|
||||
@@ -55,6 +57,7 @@ pub struct IpInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct LevelInfoInPlayerInfo {
|
||||
pub current_level: i64,
|
||||
pub current_min: i64,
|
||||
@@ -64,6 +67,7 @@ pub struct LevelInfoInPlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct VipInPlayerInfo {
|
||||
#[serde(rename = "type")]
|
||||
@@ -84,6 +88,7 @@ pub struct VipInPlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct LabelInPlayerInfo {
|
||||
pub path: String,
|
||||
@@ -101,14 +106,17 @@ pub struct LabelInPlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct AvatarIcon {
|
||||
pub icon_resource: IconResource,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct IconResource {}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SubtitleInPlayerInfo {
|
||||
pub allow_submit: bool,
|
||||
pub lan: String,
|
||||
@@ -117,6 +125,7 @@ pub struct SubtitleInPlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SubtitleDetailInPlayerInfo {
|
||||
pub id: i64,
|
||||
pub lan: String,
|
||||
@@ -131,6 +140,7 @@ pub struct SubtitleDetailInPlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct ViewPoint {
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: i64,
|
||||
@@ -144,12 +154,14 @@ pub struct ViewPoint {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Options {
|
||||
pub is_360: bool,
|
||||
pub without_vip: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct OnlineSwitch {
|
||||
pub enable_gray_dash_playback: String,
|
||||
pub new_broadcast: String,
|
||||
@@ -158,17 +170,20 @@ pub struct OnlineSwitch {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Fawkes {
|
||||
pub config_version: i64,
|
||||
pub ff_version: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct ShowSwitch {
|
||||
pub long_progress: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct ElecHighLevel {
|
||||
pub privilege_type: i64,
|
||||
pub title: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct QrcodeData {
|
||||
pub url: String,
|
||||
pub qrcode_key: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct QrcodeStatus {
|
||||
pub url: String,
|
||||
pub refresh_token: String,
|
||||
|
||||
@@ -19,22 +19,27 @@ pub enum SearchResult {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct NormalSearchResult(pub NormalInfo);
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct BangumiSearchResult {
|
||||
pub ep: Option<bangumi_info::EpInBangumi>,
|
||||
pub info: BangumiInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct CheeseSearchResult {
|
||||
pub ep: Option<cheese_info::EpInCheese>,
|
||||
pub info: CheeseInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserVideoSearchResult(pub UserVideoInfo);
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct FavSearchResult(pub FavInfo);
|
||||
|
||||
@@ -4,9 +4,11 @@ use specta::Type;
|
||||
use crate::downloader::chapter_segments::ChapterSegment;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SkipSegments(pub Vec<SkipSegment>);
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct SkipSegment {
|
||||
pub cid: String,
|
||||
pub category: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Subtitle {
|
||||
pub font_size: f64,
|
||||
pub font_color: String,
|
||||
@@ -13,6 +14,7 @@ pub struct Subtitle {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Body {
|
||||
pub from: f64,
|
||||
pub to: f64,
|
||||
|
||||
@@ -4,6 +4,7 @@ use specta::Type;
|
||||
pub type Tags = Vec<Tag>;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Tag {
|
||||
pub tag_id: i64,
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserInfo {
|
||||
#[serde(rename = "isLogin")]
|
||||
pub is_login: bool,
|
||||
@@ -42,6 +43,7 @@ pub struct UserInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct LevelInfoInUserInfo {
|
||||
pub current_level: i64,
|
||||
@@ -50,6 +52,7 @@ pub struct LevelInfoInUserInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Official {
|
||||
pub role: i64,
|
||||
pub title: String,
|
||||
@@ -59,6 +62,7 @@ pub struct Official {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct OfficialVerify {
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: i64,
|
||||
@@ -66,6 +70,7 @@ pub struct OfficialVerify {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PendantInUserInfo {
|
||||
pub pid: i64,
|
||||
pub name: String,
|
||||
@@ -77,6 +82,7 @@ pub struct PendantInUserInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct VipLabel {
|
||||
pub path: String,
|
||||
pub text: String,
|
||||
@@ -93,6 +99,7 @@ pub struct VipLabel {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct VipInUserInfo {
|
||||
#[serde(rename = "type")]
|
||||
@@ -112,6 +119,7 @@ pub struct VipInUserInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct LabelInUserInfo {
|
||||
pub path: String,
|
||||
@@ -129,9 +137,11 @@ pub struct LabelInUserInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct IconResource {}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct Wallet {
|
||||
pub mid: i64,
|
||||
pub bcoin_balance: f64,
|
||||
@@ -140,6 +150,7 @@ pub struct Wallet {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct WbiImg {
|
||||
pub img_url: String,
|
||||
pub sub_url: String,
|
||||
|
||||
@@ -2,17 +2,20 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserVideoInfo {
|
||||
pub list: UserVideoList,
|
||||
pub page: PageInUserVideo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct UserVideoList {
|
||||
pub vlist: Vec<EpInUserVideo>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct EpInUserVideo {
|
||||
pub comment: i64,
|
||||
pub typeid: i64,
|
||||
@@ -54,6 +57,7 @@ pub struct EpInUserVideo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct MetaInUserVideo {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
@@ -70,6 +74,7 @@ pub struct MetaInUserVideo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInUserVideo {
|
||||
pub season_id: i64,
|
||||
pub view: i64,
|
||||
@@ -85,6 +90,7 @@ pub struct StatInUserVideo {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PageInUserVideo {
|
||||
pub pn: i64,
|
||||
pub ps: i64,
|
||||
|
||||
@@ -7,6 +7,8 @@ use specta::Type;
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
|
||||
@@ -2,12 +2,14 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct WatchLaterInfo {
|
||||
pub count: i64,
|
||||
pub list: Vec<MediaInWatchLater>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct MediaInWatchLater {
|
||||
pub aid: i64,
|
||||
pub videos: i64,
|
||||
@@ -64,6 +66,7 @@ pub struct MediaInWatchLater {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct RightsInWatchLater {
|
||||
pub bp: i64,
|
||||
pub elec: i64,
|
||||
@@ -82,6 +85,7 @@ pub struct RightsInWatchLater {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct OwnerInWatchLater {
|
||||
pub mid: i64,
|
||||
pub name: String,
|
||||
@@ -89,6 +93,7 @@ pub struct OwnerInWatchLater {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct StatInWatchLater {
|
||||
pub aid: i64,
|
||||
pub view: i64,
|
||||
@@ -106,6 +111,7 @@ pub struct StatInWatchLater {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct DimensionInWatchLater {
|
||||
pub width: i64,
|
||||
pub height: i64,
|
||||
@@ -113,6 +119,7 @@ pub struct DimensionInWatchLater {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default)]
|
||||
pub struct PageInWatchLater {
|
||||
pub cid: i64,
|
||||
pub page: i64,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "bilibili-video-downloader",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.lanyeeee.bilibili-video-downloader",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import AppContent from './AppContent.vue'
|
||||
import { GlobalThemeOverrides } from 'naive-ui'
|
||||
import { GlobalThemeOverrides, zhCN, dateZhCN } from 'naive-ui'
|
||||
|
||||
const themeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
@@ -15,6 +15,10 @@ const themeOverrides: GlobalThemeOverrides = {
|
||||
Tabs: {
|
||||
tabGapSmallLine: '10px',
|
||||
tabPaddingSmallLine: '6px 8px',
|
||||
tabTextColorActiveSegment: '#00AEECFF',
|
||||
tabTextColorHoverSegment: '#00AEECFF',
|
||||
tabColorSegment: '#DFF6FDFF',
|
||||
colorSegment: '#FFFFFFFF',
|
||||
},
|
||||
Button: {
|
||||
paddingSmall: '0 8px',
|
||||
@@ -35,7 +39,7 @@ const themeOverrides: GlobalThemeOverrides = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themeOverrides">
|
||||
<n-config-provider :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-dialog-provider>
|
||||
<n-modal-provider>
|
||||
<n-notification-provider placement="bottom-right" :max="3">
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PhClock,
|
||||
PhHeart,
|
||||
PhDownload,
|
||||
PhPlayCircle,
|
||||
} from '@phosphor-icons/vue'
|
||||
import AboutDialog from './dialogs/AboutDialog.vue'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
@@ -22,8 +23,9 @@ import WatchLaterPane from './panes/WatchLaterPane/WatchLaterPane.vue'
|
||||
import DownloadPane from './panes/DownloadPane/DownloadPane.vue'
|
||||
import { searchPaneRefKey, navDownloadButtonRefKey } from './injection_keys.ts'
|
||||
import BangumiFollowPane from './panes/BangumiFollow/BangumiFollowPane.vue'
|
||||
import HistoryPane from './panes/HistoryPane/HistoryPane.vue'
|
||||
|
||||
export type CurrentNavName = 'search' | 'fav' | 'watch_later' | 'bangumi_follow' | 'download'
|
||||
export type CurrentNavName = 'search' | 'fav' | 'history' | 'bangumi_follow' | 'watch_later' | 'download'
|
||||
|
||||
const currentPlatform = platform()
|
||||
|
||||
@@ -58,12 +60,13 @@ onMounted(() => {
|
||||
<TitleBar />
|
||||
|
||||
<div v-if="store.config !== undefined" class="h-full w-full flex overflow-hidden select-none">
|
||||
<div class="flex flex-col box-border p-1 border-r-solid border-r-1 border-r-[#DADADA] bg-[#F9F9F9] flex-shrink-0">
|
||||
<div
|
||||
class="flex flex-col px-1 box-border border-r-solid border-r-1 border-r-[#DADADA] bg-[#F9F9F9] flex-shrink-0">
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
搜索
|
||||
<template #trigger>
|
||||
<div
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded p-1 my-1"
|
||||
@click="store.currentNavName = 'search'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'search' }">
|
||||
<PhMagnifyingGlass :weight="store.currentNavName === 'search' ? 'fill' : 'regular'" size="28" />
|
||||
@@ -72,10 +75,10 @@ onMounted(() => {
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
收藏
|
||||
收藏夹
|
||||
<template #trigger>
|
||||
<div
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded p-1 my-1"
|
||||
@click="store.currentNavName = 'fav'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'fav' }">
|
||||
<PhStar :weight="store.currentNavName === 'fav' ? 'fill' : 'regular'" size="28" />
|
||||
@@ -84,13 +87,13 @@ onMounted(() => {
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
稍后再看
|
||||
历史记录
|
||||
<template #trigger>
|
||||
<div
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||
@click="store.currentNavName = 'watch_later'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'watch_later' }">
|
||||
<PhClock :weight="store.currentNavName === 'watch_later' ? 'fill' : 'regular'" size="28" />
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded p-1 my-1"
|
||||
@click="store.currentNavName = 'history'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'history' }">
|
||||
<PhClock :weight="store.currentNavName === 'history' ? 'fill' : 'regular'" size="28" />
|
||||
</div>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
@@ -99,7 +102,7 @@ onMounted(() => {
|
||||
追番追剧
|
||||
<template #trigger>
|
||||
<div
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded p-1 my-1"
|
||||
@click="store.currentNavName = 'bangumi_follow'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'bangumi_follow' }">
|
||||
<PhHeart :weight="store.currentNavName === 'bangumi_follow' ? 'fill' : 'regular'" size="28" />
|
||||
@@ -108,12 +111,24 @@ onMounted(() => {
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
下载
|
||||
稍后再看
|
||||
<template #trigger>
|
||||
<div
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded p-1 my-1"
|
||||
@click="store.currentNavName = 'watch_later'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'watch_later' }">
|
||||
<PhPlayCircle :weight="store.currentNavName === 'watch_later' ? 'fill' : 'regular'" size="28" />
|
||||
</div>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
下载任务
|
||||
<template #trigger>
|
||||
<n-badge :value="store.uncompletedProgressesCount" :offset="[-7, 7]">
|
||||
<div
|
||||
ref="downloadButtonRef"
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded p-1 my-1"
|
||||
@click="store.currentNavName = 'download'"
|
||||
:class="{ 'text-sky-5': store.currentNavName === 'download' }">
|
||||
<PhDownload :weight="store.currentNavName === 'download' ? 'fill' : 'regular'" size="28" />
|
||||
@@ -125,7 +140,7 @@ onMounted(() => {
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
配置
|
||||
<template #trigger>
|
||||
<n-button text class="mt-auto py-1 px-2" @click="settingsDialogShowing = true">
|
||||
<n-button text class="mt-auto p-1" @click="settingsDialogShowing = true">
|
||||
<n-icon size="28">
|
||||
<PhGearSix />
|
||||
</n-icon>
|
||||
@@ -136,7 +151,7 @@ onMounted(() => {
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
日志
|
||||
<template #trigger>
|
||||
<n-button text class="py-1 px-2" @click="logDialogShowing = true">
|
||||
<n-button text class="p-1" @click="logDialogShowing = true">
|
||||
<n-icon size="28">
|
||||
<PhClockCounterClockwise />
|
||||
</n-icon>
|
||||
@@ -147,7 +162,7 @@ onMounted(() => {
|
||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||
关于
|
||||
<template #trigger>
|
||||
<n-button text class="py-1 px-2 mb-2" @click="aboutDialogShowing = true">
|
||||
<n-button text class="p-1 mb-1" @click="aboutDialogShowing = true">
|
||||
<n-icon size="28">
|
||||
<PhInfo />
|
||||
</n-icon>
|
||||
@@ -163,11 +178,14 @@ onMounted(() => {
|
||||
<FavPane class="absolute inset-0" v-show="store.currentNavName === 'fav'" />
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<WatchLaterPane class="absolute inset-0" v-show="store.currentNavName === 'watch_later'" />
|
||||
<HistoryPane class="absolute inset-0" v-show="store.currentNavName === 'history'" />
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<BangumiFollowPane class="absolute inset-0" v-show="store.currentNavName === 'bangumi_follow'" />
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<WatchLaterPane class="absolute inset-0" v-show="store.currentNavName === 'watch_later'" />
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<DownloadPane class="absolute inset-0" v-show="store.currentNavName === 'download'" />
|
||||
</transition>
|
||||
|
||||
@@ -96,6 +96,14 @@ async getBangumiFollowInfo(params: GetBangumiFollowInfoParams) : Promise<Result<
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getHistoryInfo(params: GetHistoryInfoParams) : Promise<Result<HistoryInfo, CommandError>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_history_info", { params }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async createDownloadTasks(params: CreateDownloadTaskParams) : Promise<void> {
|
||||
await TAURI_INVOKE("create_download_tasks", { params });
|
||||
},
|
||||
@@ -247,7 +255,7 @@ export type CntInfo = { collect: number; play: number; thumb_up: number; share:
|
||||
export type CntInfoInMedia = { collect: number; play: number; danmaku: number; vt: number; play_switch: number; reply: number; view_text_1: string }
|
||||
export type CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1"
|
||||
export type CommandError = { err_title: string; err_message: string }
|
||||
export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig }
|
||||
export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; video_quality_priority: VideoQuality[]; codec_type_priority: CodecType[]; audio_quality_priority: AudioQuality[]; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig }
|
||||
export type Consulting = { consulting_flag: boolean; consulting_url: string }
|
||||
export type ContentAttr = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg }
|
||||
export type ContentList = { bold: boolean; content: string; number: string }
|
||||
@@ -259,6 +267,7 @@ export type CreateDownloadTaskParams = { Normal: CreateNormalDownloadTaskParams
|
||||
export type CreateNormalDownloadTaskParams = { info: NormalInfo; aid_cid_pairs: ([number, number | null])[] }
|
||||
export type DanmakuTask = { xml_selected: boolean; ass_selected: boolean; json_selected: boolean; completed: boolean }
|
||||
export type DescV2 = { raw_text: string; type: number; biz_id: number }
|
||||
export type DeviceType = "All" | "PC" | "Mobile" | "Pad" | "TV"
|
||||
export type Dimension = { width: number; height: number; rotate: number }
|
||||
export type DimensionInBangumi = { height: number; rotate: number; width: number }
|
||||
export type DimensionInWatchLater = { width: number; height: number; rotate: number }
|
||||
@@ -290,8 +299,12 @@ type: number; pn: number; follow_status: number }
|
||||
export type GetBangumiInfoParams = { EpId: number } | { SeasonId: number }
|
||||
export type GetCheeseInfoParams = { EpId: number } | { SeasonId: number }
|
||||
export type GetFavInfoParams = { media_list_id: number; pn: number }
|
||||
export type GetHistoryInfoParams = { pn: number; keyword: string; add_time_start: number; add_time_end: number; arc_max_duration: number; arc_min_duration: number; device_type: DeviceType }
|
||||
export type GetNormalInfoParams = { Bvid: string } | { Aid: number }
|
||||
export type GetUserVideoInfoParams = { pn: number; mid: number }
|
||||
export type History = { oid: number; epid: number; bvid: string; page: number; cid: number; part: string; business: string; dt: number }
|
||||
export type HistoryDetail = { title: string; long_title: string; cover: string; uri: string; history: History; videos: number; author_name: string; author_face: string; author_mid: number; view_at: number; progress: number; badge: string; show_title: string; duration: number; total: number; new_desc: string; is_finish: number; is_fav: number; kid: number; tag_name: string; live_status: number }
|
||||
export type HistoryInfo = { has_more: boolean; page: PageInHistory; list: HistoryDetail[] | null }
|
||||
export type Honor = { aid: number; type: number; desc: string; weekly_recommend_num: number }
|
||||
export type HonorReply = { honor: Honor[] | null }
|
||||
export type IconFont = { name: string; text: string }
|
||||
@@ -319,6 +332,7 @@ export type OfficialVerify = { type: number; desc: string }
|
||||
export type Op = { end: number; start: number }
|
||||
export type OwnerInNormal = { mid: number; name: string; face: string }
|
||||
export type OwnerInWatchLater = { mid: number; name: string; face: string }
|
||||
export type PageInHistory = { pn: number; total: number }
|
||||
export type PageInNormal = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: Dimension; ctime: number }
|
||||
export type PageInNormalEp = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: Dimension }
|
||||
export type PageInUserVideo = { pn: number; ps: number; count: number }
|
||||
@@ -331,9 +345,6 @@ export type PendantInCheese = { image: string; name: string; pid: number }
|
||||
export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number }
|
||||
export type PlayStrategy = { strategies: string[] }
|
||||
export type Positive = { id: number; title: string }
|
||||
export type PreferAudioQuality = "Best" | "64K" | "132K" | "192K" | "Dolby" | "HiRes"
|
||||
export type PreferCodecType = "Unknown" | "AVC" | "HEVC" | "AV1"
|
||||
export type PreferVideoQuality = "Best" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K"
|
||||
export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string }
|
||||
export type Producer = { mid: number; type: number; is_contribute: number | null; title: string }
|
||||
export type ProxyMode = "NoProxy" | "System" | "Custom"
|
||||
|
||||
21
src/components/IconButton.vue
Normal file
21
src/components/IconButton.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
href?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
v-if="href !== undefined"
|
||||
:href="href"
|
||||
target="_blank"
|
||||
draggable="false"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6 active:text-white">
|
||||
<slot />
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6 active:text-white">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,83 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectBaseOption } from 'naive-ui/es/select/src/interface'
|
||||
import { PreferAudioQuality, PreferVideoQuality } from '../../../bindings.ts'
|
||||
import { AudioQuality, VideoQuality, CodecType } from '../../../bindings.ts'
|
||||
import { useStore } from '../../../store.ts'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import ColorfulTag from '../../../components/ColorfulTag.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const videoQualitySelectOptions: SelectBaseOption<PreferVideoQuality>[] = [
|
||||
{ label: '最高', value: 'Best' },
|
||||
{ label: '240P', value: '240P' },
|
||||
{ label: '360P', value: '360P' },
|
||||
{ label: '480P', value: '480P' },
|
||||
{ label: '720P', value: '720P' },
|
||||
{ label: '720P 60帧', value: '720P60' },
|
||||
{ label: '1080P', value: '1080P' },
|
||||
{ label: '智能修复', value: 'AiRepair' },
|
||||
{ label: '1080P 高码率', value: '1080P+' },
|
||||
{ label: '1080P 60帧', value: '1080P60' },
|
||||
{ label: '4K', value: '4K' },
|
||||
{ label: 'HDR', value: 'HDR' },
|
||||
{ label: '杜比视界', value: 'Dolby' },
|
||||
{ label: '8K', value: '8K' },
|
||||
]
|
||||
const audioQualitySelectOptions: SelectBaseOption<PreferAudioQuality>[] = [
|
||||
{ label: '最高', value: 'Best' },
|
||||
{ label: '64K', value: '64K' },
|
||||
{ label: '132K', value: '132K' },
|
||||
{ label: '192K', value: '192K' },
|
||||
{ label: '杜比全景声', value: 'Dolby' },
|
||||
{ label: 'Hi-Res', value: 'HiRes' },
|
||||
]
|
||||
const videoQualityNameMap: Map<VideoQuality, string> = new Map([
|
||||
['240P', '240P 极速'],
|
||||
['360P', '360P 流畅'],
|
||||
['480P', '480P 标清'],
|
||||
['720P', '720P 准高清'],
|
||||
['720P60', '720P 60帧'],
|
||||
['1080P', '1080P 高清'],
|
||||
['AiRepair', 'AI智能修复'],
|
||||
['1080P+', '1080P 高码率'],
|
||||
['1080P60', '1080P 60帧'],
|
||||
['4K', '4K 超高清'],
|
||||
['HDR', 'HDR 真彩色'],
|
||||
['Dolby', '杜比视界'],
|
||||
['8K', '8K 超高清'],
|
||||
])
|
||||
|
||||
const audioQualityNameMap: Map<AudioQuality, string> = new Map([
|
||||
['64K', '64K'],
|
||||
['132K', '132K'],
|
||||
['192K', '192K'],
|
||||
['Dolby', '杜比全景声'],
|
||||
['HiRes', 'Hi-Res 无损'],
|
||||
])
|
||||
|
||||
const codecTypeNameMap: Map<CodecType, string> = new Map([
|
||||
['AVC', 'AVC (H.264)'],
|
||||
['HEVC', 'HEVC (H.265)'],
|
||||
['AV1', 'AV1'],
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="store.config !== undefined" class="flex flex-col gap-row-2">
|
||||
<div class="flex gap-2">
|
||||
<span class="font-bold">主要内容</span>
|
||||
<n-checkbox v-model:checked="store.config.download_video">下载视频</n-checkbox>
|
||||
<n-checkbox v-model:checked="store.config.download_audio">下载音频</n-checkbox>
|
||||
<span class="w-15 font-bold">主要内容</span>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_video">下载视频</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_audio">下载音频</n-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<span class="font-bold">视频处理</span>
|
||||
<span class="w-15 font-bold">视频处理</span>
|
||||
<n-tooltip placement="top" trigger="hover">
|
||||
<div>自动合并音频和视频</div>
|
||||
<template #trigger>
|
||||
<n-checkbox v-model:checked="store.config.auto_merge">自动合并</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.auto_merge">自动合并</n-checkbox>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
<n-tooltip placement="top" trigger="hover">
|
||||
<div>如果视频有章节分段,则将章节信息嵌入mp4文件的元数据中</div>
|
||||
<div>使视频在各类播放器中支持章节导航(例如进度条分段)</div>
|
||||
<template #trigger>
|
||||
<n-checkbox v-model:checked="store.config.embed_chapter">标记章节</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.embed_chapter">标记章节</n-checkbox>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
<n-tooltip placement="top" trigger="hover">
|
||||
<div>将视频的广告部分以章节的形式嵌入mp4文件的元数据中</div>
|
||||
<div>可以实现自动跳过广告(如果播放器支持的话)</div>
|
||||
<template #trigger>
|
||||
<n-checkbox v-model:checked="store.config.embed_skip">标记广告</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.embed_skip">标记广告</n-checkbox>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<span class="font-bold">下载弹幕</span>
|
||||
<span class="w-15 font-bold">下载弹幕</span>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_xml_danmaku">xml弹幕</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_ass_danmaku">ass弹幕</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_json_danmaku">json弹幕</n-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<span class="font-bold">其他内容</span>
|
||||
<n-checkbox v-model:checked="store.config.download_subtitle">下载字幕</n-checkbox>
|
||||
<n-checkbox v-model:checked="store.config.download_cover">下载封面</n-checkbox>
|
||||
<span class="w-15 font-bold">其他内容</span>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_subtitle">下载字幕</n-checkbox>
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_cover">下载封面</n-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<span class="w-14 font-bold">元数据</span>
|
||||
<span class="w-14 w-15 font-bold">元数据</span>
|
||||
<n-tooltip placement="top" trigger="hover">
|
||||
<div>还会顺便下载poster和fanart(如果有的话)</div>
|
||||
<template #trigger>
|
||||
@@ -87,44 +93,71 @@ const audioQualitySelectOptions: SelectBaseOption<PreferAudioQuality>[] = [
|
||||
<n-checkbox class="w-22" v-model:checked="store.config.download_json">json刮削</n-checkbox>
|
||||
</div>
|
||||
|
||||
<n-tooltip placement="left" trigger="hover" class="w-20vw">
|
||||
<div>如果视频有对应的画质,则使用对应的画质,否则选择最高画质</div>
|
||||
<template #trigger>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2 whitespace-nowrap font-bold">优先画质</span>
|
||||
<n-select
|
||||
size="small"
|
||||
v-model:value="store.config.prefer_video_quality"
|
||||
:options="videoQualitySelectOptions" />
|
||||
<div class="flex flex-justify-between">
|
||||
<div>
|
||||
<span class="whitespace-nowrap font-bold">画质优先级</span>
|
||||
<div class="overflow-auto overflow-x-hidden h-36">
|
||||
<VueDraggable
|
||||
:force-fallback="true"
|
||||
:fallback-on-body="true"
|
||||
:animation="300"
|
||||
v-model="store.config.video_quality_priority"
|
||||
ghostClass="draggable-ghost"
|
||||
class="select-none flex flex-col gap-2">
|
||||
<ColorfulTag
|
||||
class="whitespace-nowrap cursor-move"
|
||||
color="blue"
|
||||
v-for="videoQuality in store.config.video_quality_priority"
|
||||
:key="videoQuality">
|
||||
{{ videoQualityNameMap.get(videoQuality) || videoQuality }}
|
||||
</ColorfulTag>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
|
||||
<n-tooltip placement="left" trigger="hover" class="w-20vw">
|
||||
<div>如果视频有对应的音质,则使用对应的音质,否则选择最高音质</div>
|
||||
<template #trigger>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2 whitespace-nowrap font-bold">优先音质</span>
|
||||
<n-select
|
||||
size="small"
|
||||
v-model:value="store.config.prefer_audio_quality"
|
||||
:options="audioQualitySelectOptions" />
|
||||
</div>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
<div>
|
||||
<span class="whitespace-nowrap font-bold">音质优先级</span>
|
||||
<VueDraggable
|
||||
:force-fallback="true"
|
||||
:fallback-on-body="true"
|
||||
v-model="store.config.audio_quality_priority"
|
||||
:animation="300"
|
||||
ghostClass="draggable-ghost"
|
||||
class="select-none flex flex-col gap-2">
|
||||
<ColorfulTag
|
||||
class="whitespace-nowrap cursor-move"
|
||||
color="blue"
|
||||
v-for="audioQuality in store.config.audio_quality_priority"
|
||||
:key="audioQuality">
|
||||
{{ audioQualityNameMap.get(audioQuality) || audioQuality }}
|
||||
</ColorfulTag>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
|
||||
<n-tooltip placement="left" trigger="hover" class="w-20vw">
|
||||
<div>如果视频有对应的编码,则使用对应的编码,否则按照 AVC > HEVC > AV1 的顺序选择编码</div>
|
||||
<template #trigger>
|
||||
<div>
|
||||
<span class="mr-2 font-bold">优先编码</span>
|
||||
<n-radio-group v-model:value="store.config.prefer_codec_type" size="small">
|
||||
<n-radio-button value="AVC">AVC</n-radio-button>
|
||||
<n-radio-button value="HEVC">HEVC</n-radio-button>
|
||||
<n-radio-button value="AV1">AV1</n-radio-button>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
</n-tooltip>
|
||||
<div>
|
||||
<span class="whitespace-nowrap font-bold">编码优先级</span>
|
||||
<VueDraggable
|
||||
:force-fallback="true"
|
||||
:fallback-on-body="true"
|
||||
v-model="store.config.codec_type_priority"
|
||||
:animation="300"
|
||||
ghostClass="draggable-ghost"
|
||||
class="select-none flex flex-col gap-2">
|
||||
<ColorfulTag
|
||||
class="whitespace-nowrap cursor-move"
|
||||
color="blue"
|
||||
v-for="codecType in store.config.codec_type_priority"
|
||||
:key="codecType">
|
||||
{{ codecTypeNameMap.get(codecType) || codecType }}
|
||||
</ColorfulTag>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.draggable-ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ensureHttps, isElementInViewport, playTaskToQueueAnimation } from '../.
|
||||
import { PhDownloadSimple, PhGoogleChromeLogo, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import { EpInBangumiFollow } from '../../../bindings.ts'
|
||||
import SimpleCheckbox from '../../../components/SimpleCheckbox.vue'
|
||||
import IconButton from '../../../components/IconButton.vue'
|
||||
|
||||
const searchPaneRef = inject(searchPaneRefKey)
|
||||
|
||||
@@ -18,7 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||
const rootDivRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<InstanceType<typeof IconButton>>()
|
||||
|
||||
async function handleDownloadClick() {
|
||||
if (props.downloadEpisode === undefined) {
|
||||
@@ -34,7 +35,7 @@ function playDownloadAnimation() {
|
||||
return
|
||||
}
|
||||
|
||||
const from = downloadButtonRef.value
|
||||
const from = downloadButtonRef.value?.$el
|
||||
const to = navDownloadButtonRef?.value
|
||||
|
||||
if (from instanceof Element && to !== undefined) {
|
||||
@@ -71,29 +72,20 @@ defineExpose({ playDownloadAnimation, ep: props.ep })
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 items-center mt-2">
|
||||
<a
|
||||
:href="`https://www.bilibili.com/bangumi/play/ss${ep.season_id}`"
|
||||
target="_blank"
|
||||
draggable="false"
|
||||
title="在浏览器中打开"
|
||||
class="p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6">
|
||||
<IconButton title="在浏览器中打开" :href="`https://www.bilibili.com/bangumi/play/ss${ep.season_id}`">
|
||||
<PhGoogleChromeLogo :size="24" />
|
||||
</a>
|
||||
<div
|
||||
title="在下载器内搜索"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="searchPaneRef?.search(`ss${props.ep.season_id}`, 'Bangumi')">
|
||||
</IconButton>
|
||||
<IconButton title="在下载器内搜索" @click="searchPaneRef?.search(`ss${props.ep.season_id}`, 'Bangumi')">
|
||||
<PhMagnifyingGlass :size="24" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="downloadEpisode !== undefined"
|
||||
ref="downloadButtonRef"
|
||||
class="ml-auto"
|
||||
title="一键下载"
|
||||
class="ml-auto cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="handleDownloadClick">
|
||||
<PhDownloadSimple :size="24" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,6 +19,7 @@ import ColorfulTag from '../../../components/ColorfulTag.vue'
|
||||
import { searchPaneRefKey } from '../../../injection_keys.ts'
|
||||
import { ProgressData } from '../DownloadPane.vue'
|
||||
import { ensureHttps } from '../../../utils.tsx'
|
||||
import IconButton from '../../../components/IconButton.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
@@ -153,7 +154,7 @@ function handleSearchClick() {
|
||||
P{{ p.part_order }} {{ p.part_title }}
|
||||
</ColorfulTag>
|
||||
|
||||
<div class="mt-auto flex gap-1 flex-wrap pt-2" title="下载内容">
|
||||
<div class="mt-auto flex gap-1 flex-wrap pt-2" title="任务内容">
|
||||
<ColorfulTag v-if="p.video_task.selected" color="blue">
|
||||
视频(编码:{{ p.video_task.codec_type }} 画质:{{ p.video_task.video_quality }})
|
||||
</ColorfulTag>
|
||||
@@ -161,19 +162,19 @@ function handleSearchClick() {
|
||||
音频(音质:{{ p.audio_task.audio_quality }})
|
||||
</ColorfulTag>
|
||||
|
||||
<ColorfulTag v-if="p.video_process_task.merge_selected" color="rose">自动合并</ColorfulTag>
|
||||
<ColorfulTag v-if="p.video_process_task.embed_chapter_selected" color="rose">标记章节</ColorfulTag>
|
||||
<ColorfulTag v-if="p.video_process_task.embed_skip_selected" color="rose">标记广告</ColorfulTag>
|
||||
<ColorfulTag v-if="p.video_process_task.merge_selected" color="purple">自动合并</ColorfulTag>
|
||||
<ColorfulTag v-if="p.video_process_task.embed_chapter_selected" color="purple">标记章节</ColorfulTag>
|
||||
<ColorfulTag v-if="p.video_process_task.embed_skip_selected" color="purple">标记广告</ColorfulTag>
|
||||
|
||||
<ColorfulTag v-if="p.danmaku_task.xml_selected" color="purple">xml弹幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.danmaku_task.ass_selected" color="purple">ass弹幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.danmaku_task.json_selected" color="purple">json弹幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.danmaku_task.xml_selected" color="green">xml弹幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.danmaku_task.ass_selected" color="green">ass弹幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.danmaku_task.json_selected" color="green">json弹幕</ColorfulTag>
|
||||
|
||||
<ColorfulTag v-if="p.subtitle_task.selected" color="green">字幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.cover_task.selected" color="green">封面</ColorfulTag>
|
||||
<ColorfulTag v-if="p.subtitle_task.selected" color="amber">字幕</ColorfulTag>
|
||||
<ColorfulTag v-if="p.cover_task.selected" color="amber">封面</ColorfulTag>
|
||||
|
||||
<ColorfulTag v-if="p.nfo_task.selected" color="amber">nfo刮削</ColorfulTag>
|
||||
<ColorfulTag v-if="p.json_task.selected" color="amber">json刮削</ColorfulTag>
|
||||
<ColorfulTag v-if="p.nfo_task.selected" color="rose">nfo刮削</ColorfulTag>
|
||||
<ColorfulTag v-if="p.json_task.selected" color="rose">json刮削</ColorfulTag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,34 +207,24 @@ function handleSearchClick() {
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex gap-2 items-center">
|
||||
<div
|
||||
<IconButton
|
||||
v-if="p.state === 'Completed' && p.video_task.selected"
|
||||
title="打开mp4目录"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="showMp4InFileManager(p.episode_dir, p.filename)">
|
||||
<PhFileVideo :size="24" />
|
||||
</div>
|
||||
<div
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="p.state === 'Completed'"
|
||||
title="打开下载目录"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="showEpisodeDirInFileManager(p.episode_dir)">
|
||||
<PhFolderOpen :size="24" />
|
||||
</div>
|
||||
<div
|
||||
title="在下载器内搜索"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="handleSearchClick">
|
||||
</IconButton>
|
||||
<IconButton title="在下载器内搜索" @click="handleSearchClick">
|
||||
<PhMagnifyingGlass :size="24" />
|
||||
</div>
|
||||
<a
|
||||
:href="href"
|
||||
target="_blank"
|
||||
draggable="false"
|
||||
title="在浏览器中打开"
|
||||
class="p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6">
|
||||
</IconButton>
|
||||
<IconButton title="在浏览器中打开" :href="href">
|
||||
<PhGoogleChromeLogo :size="24" />
|
||||
</a>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ensureHttps, isElementInViewport, playTaskToQueueAnimation } from '../.
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { navDownloadButtonRefKey } from '../../../injection_keys.ts'
|
||||
import { SearchType } from '../../SearchPane/SearchPane.vue'
|
||||
import IconButton from '../../../components/IconButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
media: MediaInFav
|
||||
@@ -18,7 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||
const rootDivRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<InstanceType<typeof IconButton>>()
|
||||
|
||||
const openInBrowserHref = computed<string | undefined>(() => {
|
||||
if (props.media.type === 2) {
|
||||
@@ -54,7 +55,7 @@ function playDownloadAnimation() {
|
||||
return
|
||||
}
|
||||
|
||||
const from = downloadButtonRef.value
|
||||
const from = downloadButtonRef.value?.$el
|
||||
const to = navDownloadButtonRef?.value
|
||||
|
||||
if (from instanceof Element && to !== undefined) {
|
||||
@@ -121,31 +122,20 @@ defineExpose({ playDownloadAnimation, media: props.media })
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<a
|
||||
:href="openInBrowserHref"
|
||||
target="_blank"
|
||||
draggable="false"
|
||||
title="在浏览器中打开"
|
||||
class="p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6">
|
||||
<IconButton title="在浏览器中打开" :href="openInBrowserHref">
|
||||
<PhGoogleChromeLogo :size="24" />
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="search !== undefined"
|
||||
title="在下载器内搜索"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="searchInSearchPane">
|
||||
</IconButton>
|
||||
<IconButton v-if="search !== undefined" title="在下载器内搜索" @click="searchInSearchPane">
|
||||
<PhMagnifyingGlass :size="24" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="props.downloadEpisode !== undefined"
|
||||
ref="downloadButtonRef"
|
||||
class="ml-auto"
|
||||
title="一键下载"
|
||||
class="ml-auto cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="handleDownloadClick">
|
||||
<PhDownloadSimple :size="24" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
43
src/panes/HistoryPane/HistoryPane.vue
Normal file
43
src/panes/HistoryPane/HistoryPane.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { commands, HistoryInfo } from '../../bindings.ts'
|
||||
import { useStore } from '../../store.ts'
|
||||
import HistoryPanel from './components/HistoryPanel.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const historyInfo = ref<HistoryInfo>()
|
||||
|
||||
watch(
|
||||
() => store.userInfo,
|
||||
async () => {
|
||||
if (store.userInfo === undefined) {
|
||||
historyInfo.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const result = await commands.getHistoryInfo({
|
||||
pn: 1,
|
||||
keyword: '',
|
||||
add_time_start: 0,
|
||||
add_time_end: 0,
|
||||
arc_max_duration: 0,
|
||||
arc_min_duration: 0,
|
||||
device_type: 'All',
|
||||
})
|
||||
if (result.status === 'error') {
|
||||
console.error(result.error)
|
||||
return
|
||||
}
|
||||
|
||||
historyInfo.value = result.data
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="historyInfo !== undefined" class="h-full">
|
||||
<HistoryPanel v-model:history-info="historyInfo" />
|
||||
</div>
|
||||
<n-empty v-else class="mt-2" description="请先登录" />
|
||||
</template>
|
||||
195
src/panes/HistoryPane/components/HistoryCard.vue
Normal file
195
src/panes/HistoryPane/components/HistoryCard.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { EpisodeType, HistoryDetail } from '../../../bindings.ts'
|
||||
import { SearchType } from '../../SearchPane/SearchPane.vue'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { navDownloadButtonRefKey } from '../../../injection_keys.ts'
|
||||
import { ensureHttps, isElementInViewport, playTaskToQueueAnimation } from '../../../utils.tsx'
|
||||
import { PhDownloadSimple, PhGoogleChromeLogo, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import SimpleCheckbox from '../../../components/SimpleCheckbox.vue'
|
||||
import IconButton from '../../../components/IconButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
episodeType: EpisodeType
|
||||
historyDetail: HistoryDetail
|
||||
downloadEpisode?: (historyDetail: HistoryDetail) => Promise<void>
|
||||
checkboxChecked?: (historyDetail: HistoryDetail) => boolean
|
||||
handleCheckboxClick?: (historyDetail: HistoryDetail) => void
|
||||
handleContextMenu?: (historyDetail: HistoryDetail) => void
|
||||
search?: (input: string, searchType: SearchType) => void
|
||||
}>()
|
||||
|
||||
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||
const rootDivRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<InstanceType<typeof IconButton>>()
|
||||
|
||||
const openInBrowserHref = computed<string | undefined>(() => {
|
||||
if (props.episodeType === 'Normal') {
|
||||
return `https://www.bilibili.com/video/${props.historyDetail.history.bvid}/`
|
||||
} else if (props.episodeType === 'Bangumi') {
|
||||
return `https://www.bilibili.com/bangumi/play/ep${props.historyDetail.history.epid}`
|
||||
} else if (props.episodeType === 'Cheese') {
|
||||
return `https://www.bilibili.com/cheese/play/ep${props.historyDetail.history.epid}`
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const downloadHint = computed<string | undefined>(() => {
|
||||
if (props.episodeType !== 'Normal') {
|
||||
return '下载请点击左下角放大镜按钮'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const subTitle = computed<string>(() => {
|
||||
if (props.episodeType === 'Normal') {
|
||||
return ''
|
||||
} else if (props.historyDetail.long_title !== '') {
|
||||
return props.historyDetail.long_title
|
||||
} else if (props.historyDetail.show_title !== '') {
|
||||
return props.historyDetail.show_title
|
||||
} else if (props.historyDetail.new_desc !== '') {
|
||||
return props.historyDetail.new_desc
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const timeFormat = computed(() => {
|
||||
function isToday(date: Date): boolean {
|
||||
const today = new Date()
|
||||
return (
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
)
|
||||
}
|
||||
|
||||
function isYesterday(date: Date): boolean {
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
return (
|
||||
date.getDate() === yesterday.getDate() &&
|
||||
date.getMonth() === yesterday.getMonth() &&
|
||||
date.getFullYear() === yesterday.getFullYear()
|
||||
)
|
||||
}
|
||||
|
||||
const date = new Date(props.historyDetail.view_at * 1000)
|
||||
|
||||
if (isToday(date)) {
|
||||
return "'今天' HH:mm"
|
||||
}
|
||||
if (isYesterday(date)) {
|
||||
return "'昨天' HH:mm"
|
||||
} else {
|
||||
return 'MM-dd HH:mm'
|
||||
}
|
||||
})
|
||||
|
||||
async function handleDownloadClick() {
|
||||
if (props.downloadEpisode === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await props.downloadEpisode(props.historyDetail)
|
||||
playDownloadAnimation()
|
||||
}
|
||||
|
||||
function playDownloadAnimation() {
|
||||
if (rootDivRef.value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const from = downloadButtonRef.value?.$el
|
||||
const to = navDownloadButtonRef?.value
|
||||
|
||||
if (from instanceof Element && to !== undefined) {
|
||||
if (isElementInViewport(rootDivRef.value)) {
|
||||
// 只有卡片在视口内才播放动画
|
||||
playTaskToQueueAnimation(from, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function searchInSearchPane() {
|
||||
if (props.search === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.episodeType === 'Normal') {
|
||||
props.search(props.historyDetail.history.bvid, 'Normal')
|
||||
} else if (props.episodeType === 'Bangumi') {
|
||||
props.search(`ep${props.historyDetail.history.epid}`, 'Bangumi')
|
||||
} else if (props.episodeType === 'Cheese') {
|
||||
props.search(`ep${props.historyDetail.history.epid}`, 'Cheese')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ playDownloadAnimation, historyDetail: props.historyDetail })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-200px relative p-3 rounded-lg"
|
||||
ref="rootDivRef"
|
||||
:title="downloadHint"
|
||||
@contextmenu="handleContextMenu?.(historyDetail)">
|
||||
<SimpleCheckbox
|
||||
v-if="episodeType === 'Normal' && handleCheckboxClick !== undefined && checkboxChecked !== undefined"
|
||||
class="absolute top-6 left-6 z-1 backdrop-blur-2"
|
||||
:checked="checkboxChecked(historyDetail)"
|
||||
:on-click="() => handleCheckboxClick?.(historyDetail)" />
|
||||
<div v-if="historyDetail.badge !== ''" class="absolute top-6 right-6 z-1 bg-[#ff6699] text-white px-1 rounded">
|
||||
{{ historyDetail.badge }}
|
||||
</div>
|
||||
<img
|
||||
class="w-200px h-125px rounded-lg object-cover lazyload"
|
||||
:data-src="`${ensureHttps(historyDetail.cover)}@672w_378h_1c.webp`"
|
||||
:key="historyDetail.cover"
|
||||
alt=""
|
||||
draggable="false" />
|
||||
|
||||
<div class="w-full flex flex-col h-45px mt-2">
|
||||
<span class="line-clamp-2" :title="historyDetail.title">{{ historyDetail.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center whitespace-nowrap text-gray text-12px w-full overflow-hidden">
|
||||
<a
|
||||
v-if="episodeType === 'Normal'"
|
||||
class="min-w-0 color-inherit no-underline hover:text-sky-5 mr-1"
|
||||
:href="`https://space.bilibili.com/${historyDetail.author_mid}`"
|
||||
target="_blank"
|
||||
draggable="false">
|
||||
<div class="truncate text-ellipsis" :title="historyDetail.author_name">{{ historyDetail.author_name }}</div>
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="min-w-0 color-inherit no-underline hover:text-sky-5 mr-1"
|
||||
:href="openInBrowserHref"
|
||||
target="_blank"
|
||||
draggable="false">
|
||||
<div class="truncate text-ellipsis" :title="subTitle">{{ subTitle }}</div>
|
||||
</a>
|
||||
|
||||
<span class="ml-auto flex-shrink-0" title="上次观看时间">
|
||||
<n-time unix :format="timeFormat" :time="historyDetail.view_at" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<IconButton title="在浏览器中打开" :href="openInBrowserHref">
|
||||
<PhGoogleChromeLogo :size="24" />
|
||||
</IconButton>
|
||||
<IconButton v-if="search !== undefined" title="在下载器内搜索" @click="searchInSearchPane">
|
||||
<PhMagnifyingGlass :size="24" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="props.downloadEpisode !== undefined"
|
||||
ref="downloadButtonRef"
|
||||
class="ml-auto"
|
||||
title="一键下载"
|
||||
@click="handleDownloadClick">
|
||||
<PhDownloadSimple :size="24" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
359
src/panes/HistoryPane/components/HistoryPanel.vue
Normal file
359
src/panes/HistoryPane/components/HistoryPanel.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<script setup lang="ts">
|
||||
import { commands, DeviceType, HistoryDetail, HistoryInfo } from '../../../bindings.ts'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { searchPaneRefKey } from '../../../injection_keys.ts'
|
||||
import HistoryCard from './HistoryCard.vue'
|
||||
import { useEpisodeDropdown, useEpisodeSelection } from '../../../utils.tsx'
|
||||
import { SelectionArea } from '@viselect/vue'
|
||||
import FloatLabelInput from '../../../components/FloatLabelInput.vue'
|
||||
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
|
||||
const historyInfo = defineModel<HistoryInfo>('historyInfo', { required: true })
|
||||
|
||||
const searchPaneRef = inject(searchPaneRefKey)
|
||||
|
||||
const currentPage = ref<number>(1)
|
||||
const pageCount = computed<number>(() => Math.ceil(historyInfo.value.page.total / 20))
|
||||
|
||||
const searching = ref<boolean>(false)
|
||||
|
||||
const searchInput = ref<string>('')
|
||||
|
||||
type DurationTabName = 'all' | '<10' | '10-30' | '30-60' | '>60'
|
||||
const durationTabName = ref<DurationTabName>('all')
|
||||
let addTimeStart: number = 0
|
||||
let addTimeEnd: number = 0
|
||||
|
||||
const datePickerRange = ref<[number, number]>(getInitRange())
|
||||
type StartTimeTabName = 'all' | 'today' | 'yesterday' | 'week' | 'date-picker'
|
||||
const startTimeTabName = ref<StartTimeTabName>('all')
|
||||
let arcMinDuration: number = 0
|
||||
let arcMaxDuration: number = 0
|
||||
|
||||
const selectedDeviceType = ref<DeviceType>('All')
|
||||
|
||||
watch(durationTabName, () => {
|
||||
if (durationTabName.value === 'all') {
|
||||
arcMinDuration = 0
|
||||
arcMaxDuration = 0
|
||||
} else if (durationTabName.value === '<10') {
|
||||
arcMinDuration = 0
|
||||
arcMaxDuration = 10 * 60
|
||||
} else if (durationTabName.value === '10-30') {
|
||||
arcMinDuration = 10 * 60
|
||||
arcMaxDuration = 30 * 60
|
||||
} else if (durationTabName.value === '30-60') {
|
||||
arcMinDuration = 30 * 60
|
||||
arcMaxDuration = 60 * 60
|
||||
} else if (durationTabName.value === '>60') {
|
||||
arcMinDuration = 60 * 60
|
||||
arcMaxDuration = 0
|
||||
}
|
||||
|
||||
getHistory(1)
|
||||
})
|
||||
|
||||
watch(startTimeTabName, () => {
|
||||
const tabName = startTimeTabName.value
|
||||
if (tabName === 'date-picker') {
|
||||
return
|
||||
}
|
||||
|
||||
if (tabName === 'all') {
|
||||
addTimeStart = 0
|
||||
addTimeEnd = 0
|
||||
} else if (tabName === 'today') {
|
||||
const now = new Date()
|
||||
addTimeStart = Math.floor(new Date(now.setHours(0, 0, 0, 0)).getTime() / 1000)
|
||||
addTimeEnd = 0
|
||||
} else if (tabName === 'yesterday') {
|
||||
const now = new Date()
|
||||
const yesterday = new Date(now.setDate(now.getDate() - 1))
|
||||
addTimeStart = Math.floor(new Date(yesterday.setHours(0, 0, 0, 0)).getTime() / 1000)
|
||||
addTimeEnd = Math.floor(new Date(yesterday.setHours(23, 59, 59, 0)).getTime() / 1000)
|
||||
} else if (tabName === 'week') {
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now.setDate(now.getDate() - 7))
|
||||
addTimeStart = Math.floor(new Date(weekAgo.setHours(0, 0, 0, 0)).getTime() / 1000)
|
||||
addTimeEnd = 0
|
||||
}
|
||||
|
||||
getHistory(1)
|
||||
})
|
||||
|
||||
watch(selectedDeviceType, () => getHistory(1))
|
||||
|
||||
async function getHistory(page: number) {
|
||||
currentPage.value = page
|
||||
searching.value = true
|
||||
|
||||
const result = await commands.getHistoryInfo({
|
||||
pn: page,
|
||||
keyword: searchInput.value,
|
||||
add_time_start: addTimeStart,
|
||||
add_time_end: addTimeEnd,
|
||||
arc_min_duration: arcMinDuration,
|
||||
arc_max_duration: arcMaxDuration,
|
||||
device_type: selectedDeviceType.value,
|
||||
})
|
||||
if (result.status === 'error') {
|
||||
console.error(result.error)
|
||||
searching.value = false
|
||||
return
|
||||
}
|
||||
historyInfo.value = result.data
|
||||
searching.value = false
|
||||
}
|
||||
|
||||
const historyCardRefs = ref<InstanceType<typeof HistoryCard>[]>([])
|
||||
const historyCardRefsMap = computed<Map<number, InstanceType<typeof HistoryCard>>>(() => {
|
||||
const map = new Map<number, InstanceType<typeof HistoryCard>>()
|
||||
historyCardRefs.value.forEach((card) => map.set(card.historyDetail.kid, card))
|
||||
return map
|
||||
})
|
||||
|
||||
const { selectedIds, updateSelectedIds, unselectAll } = useEpisodeSelection()
|
||||
const selectionAreaRef = ref<InstanceType<typeof SelectionArea>>()
|
||||
const checkedIds = ref<Set<number>>(new Set())
|
||||
|
||||
watch(historyInfo, () => {
|
||||
selectedIds.value.clear()
|
||||
checkedIds.value.clear()
|
||||
selectionAreaRef.value?.selection?.clearSelection()
|
||||
selectionAreaRef.value?.$el.scrollTo({ top: 0, behavior: 'instant' })
|
||||
})
|
||||
|
||||
const { dropdownX, dropdownY, dropdownShowing, dropdownOptions, showDropdown } = useEpisodeDropdown(
|
||||
() => {
|
||||
selectedIds.value.forEach((aid) => checkedIds.value.add(aid))
|
||||
dropdownShowing.value = false
|
||||
},
|
||||
() => {
|
||||
selectedIds.value.forEach((aid) => checkedIds.value.delete(aid))
|
||||
dropdownShowing.value = false
|
||||
},
|
||||
() => {
|
||||
historyInfo.value.list?.forEach((detail) => selectedIds.value.add(detail.kid))
|
||||
dropdownShowing.value = false
|
||||
},
|
||||
)
|
||||
|
||||
const { downloadEpisode, checkboxChecked, handleCheckboxClick, handleContextMenu } = useFavCard(
|
||||
async (historyDetail: HistoryDetail) => {
|
||||
await downloadNormalEpisode(historyDetail.kid)
|
||||
},
|
||||
(historyDetail: HistoryDetail) => {
|
||||
return checkedIds.value.has(historyDetail.kid)
|
||||
},
|
||||
(historyDetail: HistoryDetail) => {
|
||||
const checked = checkedIds.value.has(historyDetail.kid)
|
||||
if (checked) {
|
||||
checkedIds.value.delete(historyDetail.kid)
|
||||
} else {
|
||||
checkedIds.value.add(historyDetail.kid)
|
||||
}
|
||||
},
|
||||
(historyDetail: HistoryDetail) => {
|
||||
if (selectedIds.value.has(historyDetail.kid)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedIds.value.clear()
|
||||
selectedIds.value.add(historyDetail.kid)
|
||||
const selection = selectionAreaRef.value?.selection
|
||||
if (selection) {
|
||||
selection.clearSelection()
|
||||
selection.select(`[data-key="${historyDetail.kid}"]`)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function downloadNormalEpisode(aid: number) {
|
||||
// 获取普通视频信息,用于创建下载任务
|
||||
const result = await commands.getNormalInfo({ Aid: aid })
|
||||
if (result.status === 'error') {
|
||||
console.error(result.error)
|
||||
return
|
||||
}
|
||||
// 创建下载任务
|
||||
await commands.createDownloadTasks({ Normal: { info: result.data, aid_cid_pairs: [[aid, null]] } })
|
||||
}
|
||||
|
||||
async function downloadCheckedEpisodes() {
|
||||
for (const aid of checkedIds.value) {
|
||||
// 创建下载任务
|
||||
await downloadNormalEpisode(aid)
|
||||
// 播放下载动画
|
||||
const card = historyCardRefsMap.value.get(aid)
|
||||
if (card !== undefined) {
|
||||
card.playDownloadAnimation()
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimePickerConfirm(range: [number, number] | null) {
|
||||
if (range === null) {
|
||||
return
|
||||
}
|
||||
|
||||
addTimeStart = Math.floor(new Date(range[0]).setHours(0, 0, 0, 0) / 1000)
|
||||
addTimeEnd = Math.floor(new Date(range[1]).setHours(23, 59, 59, 0) / 1000)
|
||||
|
||||
startTimeTabName.value = 'date-picker'
|
||||
getHistory(1)
|
||||
}
|
||||
|
||||
function getInitRange(): [number, number] {
|
||||
const now = new Date()
|
||||
|
||||
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||
lastMonthStart.setHours(0, 0, 0, 0)
|
||||
|
||||
const todayEnd = new Date(now)
|
||||
todayEnd.setHours(23, 59, 59, 999)
|
||||
|
||||
return [lastMonthStart.getTime(), todayEnd.getTime()]
|
||||
}
|
||||
|
||||
function useFavCard(
|
||||
downloadEpisode: (historyDetail: HistoryDetail) => Promise<void>,
|
||||
checkboxChecked: (historyDetail: HistoryDetail) => boolean,
|
||||
handleCheckboxClick: (historyDetail: HistoryDetail) => void,
|
||||
handleContextMenu: (historyDetail: HistoryDetail) => void,
|
||||
) {
|
||||
return {
|
||||
downloadEpisode,
|
||||
checkboxChecked,
|
||||
handleCheckboxClick,
|
||||
handleContextMenu,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full select-none overflow-auto">
|
||||
<n-input-group class="box-border px-2 pt-2">
|
||||
<FloatLabelInput
|
||||
label="搜索标题/up主昵称"
|
||||
size="small"
|
||||
v-model:value="searchInput"
|
||||
clearable
|
||||
@keydown.enter="getHistory(1)" />
|
||||
<n-popover trigger="click" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<n-select class="w-20%" :show="false" default-value="更多筛选" size="small" />
|
||||
</template>
|
||||
<div class="w-155">
|
||||
<n-tabs class="w-111.5" type="segment" size="small" v-model:value="durationTabName">
|
||||
<n-tab name="all">全部时长</n-tab>
|
||||
<n-tab name="<10">10分钟以下</n-tab>
|
||||
<n-tab name="10-30">10-30分钟</n-tab>
|
||||
<n-tab name="30-60">30-60分钟</n-tab>
|
||||
<n-tab name=">60">60分钟以上</n-tab>
|
||||
</n-tabs>
|
||||
<div class="flex items-center">
|
||||
<n-tabs
|
||||
class="justify-between"
|
||||
type="segment"
|
||||
size="small"
|
||||
v-model:value="startTimeTabName"
|
||||
@before-leave="(tabName: StartTimeTabName) => tabName !== 'date-picker'">
|
||||
<n-tab name="all">全部时间</n-tab>
|
||||
<n-tab name="today">今天</n-tab>
|
||||
<n-tab name="yesterday">昨天</n-tab>
|
||||
<n-tab name="week">近一周</n-tab>
|
||||
<n-tab class="cursor-default!" name="date-picker">
|
||||
<n-date-picker
|
||||
size="small"
|
||||
class="ml-auto w-63.5 px-1"
|
||||
v-model:value="datePickerRange"
|
||||
type="daterange"
|
||||
@confirm="handleTimePickerConfirm" />
|
||||
</n-tab>
|
||||
</n-tabs>
|
||||
</div>
|
||||
<n-tabs class="w-111.5" type="segment" size="small" v-model:value="selectedDeviceType">
|
||||
<n-tab name="All">全部设备</n-tab>
|
||||
<n-tab name="PC">PC</n-tab>
|
||||
<n-tab name="Mobile">手机</n-tab>
|
||||
<n-tab name="TV">平板</n-tab>
|
||||
<n-tab name="Pad">TV</n-tab>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</n-popover>
|
||||
<n-button :loading="searching" type="primary" size="small" class="w-10%" @click="getHistory(1)">
|
||||
<template #icon>
|
||||
<n-icon size="22">
|
||||
<PhMagnifyingGlass weight="bold" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
<SelectionArea
|
||||
ref="selectionAreaRef"
|
||||
class="selection-container flex flex-col flex-1 px-2 overflow-auto"
|
||||
:options="{ selectables: '.selectable', features: { deselectOnBlur: true } }"
|
||||
@contextmenu="showDropdown"
|
||||
@move="updateSelectedIds"
|
||||
@start="unselectAll">
|
||||
<div class="animate-pulse text-violet">左键拖动进行框选,右键打开菜单</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template v-for="historyDetail in historyInfo.list" :key="historyDetail.kid">
|
||||
<HistoryCard
|
||||
v-if="historyDetail.badge === ''"
|
||||
ref="historyCardRefs"
|
||||
:data-key="historyDetail.kid"
|
||||
:class="[
|
||||
'selectable border border-solid border-transparent',
|
||||
selectedIds.has(historyDetail.kid) ? 'selected shadow-md' : 'hover:bg-gray-1',
|
||||
]"
|
||||
episode-type="Normal"
|
||||
:history-detail="historyDetail"
|
||||
:download-episode="downloadEpisode"
|
||||
:checkbox-checked="checkboxChecked"
|
||||
:handle-checkbox-click="handleCheckboxClick"
|
||||
:handle-context-menu="handleContextMenu"
|
||||
:search="searchPaneRef?.search" />
|
||||
|
||||
<HistoryCard
|
||||
v-else-if="historyDetail.badge === '课堂'"
|
||||
ref="historyCardRefs"
|
||||
class="border border-solid border-transparent hover:border-gray-3"
|
||||
episode-type="Cheese"
|
||||
:history-detail="historyDetail"
|
||||
:search="searchPaneRef?.search" />
|
||||
|
||||
<HistoryCard
|
||||
v-else
|
||||
ref="historyCardRefs"
|
||||
class="border border-solid border-transparent hover:border-gray-3"
|
||||
episode-type="Bangumi"
|
||||
:history-detail="historyDetail"
|
||||
:search="searchPaneRef?.search" />
|
||||
</template>
|
||||
</div>
|
||||
</SelectionArea>
|
||||
|
||||
<div class="flex gap-2 m-2 box-border">
|
||||
<n-pagination :page-count="pageCount" :page="currentPage" @update:page="getHistory($event)" />
|
||||
<n-button class="ml-auto" size="small" type="primary" @click="downloadCheckedEpisodes">下载勾选视频</n-button>
|
||||
</div>
|
||||
|
||||
<n-dropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:x="dropdownX"
|
||||
:y="dropdownY"
|
||||
:options="dropdownOptions"
|
||||
:show="dropdownShowing"
|
||||
:on-clickoutside="() => (dropdownShowing = false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selection-container .selected {
|
||||
@apply bg-[rgb(204,232,255)];
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,7 @@ import PartsDialogContent from './PartsDialogContent.vue'
|
||||
import { ensureHttps, extractBvid, isElementInViewport, playTaskToQueueAnimation } from '../../../utils.tsx'
|
||||
import { navDownloadButtonRefKey } from '../../../injection_keys.ts'
|
||||
import { SearchType } from '../SearchPane.vue'
|
||||
import IconButton from '../../../components/IconButton.vue'
|
||||
|
||||
onMounted(() => console.log('EpisodeCard mounted'))
|
||||
onUpdated(() => console.log('EpisodeCard updated'))
|
||||
@@ -51,7 +52,7 @@ const props = defineProps<{
|
||||
|
||||
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||
const rootDivRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<InstanceType<typeof IconButton>>()
|
||||
|
||||
const episodeInfo = computed<EpisodeInfo>(() => {
|
||||
if (props.episodeType === 'NormalSingle') {
|
||||
@@ -174,7 +175,7 @@ function playDownloadAnimation() {
|
||||
return
|
||||
}
|
||||
|
||||
const from = downloadButtonRef.value
|
||||
const from = downloadButtonRef.value?.$el
|
||||
const to = navDownloadButtonRef?.value
|
||||
|
||||
if (from instanceof Element && to !== undefined) {
|
||||
@@ -226,38 +227,26 @@ defineExpose({ playDownloadAnimation, episodeInfo })
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<a
|
||||
v-if="episodeInfo.href !== undefined"
|
||||
:href="episodeInfo.href"
|
||||
target="_blank"
|
||||
draggable="false"
|
||||
title="在浏览器中打开"
|
||||
class="p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6">
|
||||
<IconButton v-if="episodeInfo.href !== undefined" title="在浏览器中打开" :href="episodeInfo.href">
|
||||
<PhGoogleChromeLogo :size="24" />
|
||||
</a>
|
||||
<div
|
||||
v-if="partsButtonShowing"
|
||||
title="查看分P"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="handlePartsButtonClick">
|
||||
</IconButton>
|
||||
<IconButton v-if="partsButtonShowing" title="查看分P" @click="handlePartsButtonClick">
|
||||
<PhQueue :size="24" />
|
||||
</div>
|
||||
<div
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="search !== undefined && episodeInfo.bvid !== undefined"
|
||||
title="在下载器内搜索"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="search(episodeInfo.bvid, 'Normal')">
|
||||
<PhMagnifyingGlass :size="24" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="downloadButtonRef"
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="downloadEpisode !== undefined"
|
||||
ref="downloadButtonRef"
|
||||
title="一键下载"
|
||||
class="ml-auto cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
class="ml-auto"
|
||||
@click="handleDownloadClick">
|
||||
<PhDownloadSimple :size="24" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ensureHttps, extractEpId, isElementInViewport, playTaskToQueueAnimation
|
||||
import { PhDownloadSimple, PhGoogleChromeLogo, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import SimpleCheckbox from '../../../components/SimpleCheckbox.vue'
|
||||
import { SearchType } from '../../SearchPane/SearchPane.vue'
|
||||
import IconButton from '../../../components/IconButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
media: MediaInWatchLater
|
||||
@@ -18,7 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||
const rootDivRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<HTMLDivElement>()
|
||||
const downloadButtonRef = ref<InstanceType<typeof IconButton>>()
|
||||
|
||||
const openInBrowserHref = computed<string>(() => {
|
||||
if (props.media.redirect_url === null) {
|
||||
@@ -43,7 +44,7 @@ function playDownloadAnimation() {
|
||||
return
|
||||
}
|
||||
|
||||
const from = downloadButtonRef.value
|
||||
const from = downloadButtonRef.value?.$el
|
||||
const to = navDownloadButtonRef?.value
|
||||
|
||||
if (from instanceof Element && to !== undefined) {
|
||||
@@ -109,31 +110,20 @@ defineExpose({ playDownloadAnimation, media: props.media })
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<a
|
||||
:href="openInBrowserHref"
|
||||
target="_blank"
|
||||
draggable="false"
|
||||
title="在浏览器中打开"
|
||||
class="p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6">
|
||||
<IconButton title="在浏览器中打开" :href="openInBrowserHref">
|
||||
<PhGoogleChromeLogo :size="24" />
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="search !== undefined"
|
||||
title="在下载器内搜索"
|
||||
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
@click="searchInSearchPane">
|
||||
</IconButton>
|
||||
<IconButton v-if="search !== undefined" title="在下载器内搜索" @click="searchInSearchPane">
|
||||
<PhMagnifyingGlass :size="24" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="downloadEpisode !== undefined"
|
||||
ref="downloadButtonRef"
|
||||
title="一键下载"
|
||||
class="ml-auto cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||
class="ml-auto"
|
||||
@click="handleDownloadClick">
|
||||
<PhDownloadSimple :size="24" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user