v0.5.1: Mobile UX improvements - Custom notify styling, nav margin fixes, search button optimization
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
title: "애니 다운로더"
|
title: "애니 다운로더"
|
||||||
version: "0.4.1"
|
version: "0.4.2"
|
||||||
package_name: "anime_downloader"
|
package_name: "anime_downloader"
|
||||||
developer: "projectdx"
|
developer: "projectdx"
|
||||||
description: "anime downloader"
|
description: "anime downloader"
|
||||||
|
|||||||
49
lib/util.py
49
lib/util.py
@@ -110,7 +110,7 @@ class Util(object):
|
|||||||
i = 0
|
i = 0
|
||||||
while i < len(lines):
|
while i < len(lines):
|
||||||
line = lines[i].strip()
|
line = lines[i].strip()
|
||||||
# WEBVTT, NOTE, STYLE 등 메타데이터 스킵
|
# WEBWTT, NOTE, STYLE 등 메타데이터 스킵
|
||||||
if line.startswith("WEBVTT") or line.startswith("NOTE") or line.startswith("STYLE"):
|
if line.startswith("WEBVTT") or line.startswith("NOTE") or line.startswith("STYLE"):
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
@@ -135,3 +135,50 @@ class Util(object):
|
|||||||
# 캡션 텍스트가 바로 나오는 경우 등을 대비
|
# 캡션 텍스트가 바로 나오는 경우 등을 대비
|
||||||
i += 1
|
i += 1
|
||||||
return "\n".join(srt_lines)
|
return "\n".join(srt_lines)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def merge_subtitle(P, db_item):
|
||||||
|
"""
|
||||||
|
ffmpeg를 사용하여 SRT 자막을 MP4에 삽입 (soft embed)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
mp4_path = db_item.filepath
|
||||||
|
if not mp4_path or not os.path.exists(mp4_path):
|
||||||
|
logger.error(f"MP4 file not found: {mp4_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
srt_path = os.path.splitext(mp4_path)[0] + ".srt"
|
||||||
|
if not os.path.exists(srt_path):
|
||||||
|
logger.error(f"SRT file not found: {srt_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 출력 파일: *_subed.mp4
|
||||||
|
base_name = os.path.splitext(mp4_path)[0]
|
||||||
|
output_path = f"{base_name}_subed.mp4"
|
||||||
|
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
os.remove(output_path)
|
||||||
|
|
||||||
|
ffmpeg_cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-i", mp4_path,
|
||||||
|
"-i", srt_path,
|
||||||
|
"-c:v", "copy",
|
||||||
|
"-c:a", "copy",
|
||||||
|
"-c:s", "mov_text",
|
||||||
|
"-metadata:s:s:0", "language=kor",
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"[Merge Subtitle] Running ffmpeg: {' '.join(ffmpeg_cmd)}")
|
||||||
|
result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, timeout=600)
|
||||||
|
|
||||||
|
if result.returncode == 0 and os.path.exists(output_path):
|
||||||
|
logger.info(f"[Merge Subtitle] Success: {output_path}")
|
||||||
|
# 원본 삭제 옵션 등이 필요할 수 있으나 여기서는 생성만 함
|
||||||
|
else:
|
||||||
|
logger.error(f"ffmpeg failed: {result.stderr}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"merge_subtitle error: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|||||||
283
mod_anilife.py
283
mod_anilife.py
@@ -57,7 +57,7 @@ from .lib.crawler import Crawler
|
|||||||
|
|
||||||
# 패키지
|
# 패키지
|
||||||
# from .plugin import P
|
# from .plugin import P
|
||||||
from .lib.util import Util, yommi_timeit
|
from .lib.util import Util as AniUtil, yommi_timeit
|
||||||
from typing import Awaitable, TypeVar
|
from typing import Awaitable, TypeVar
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -78,6 +78,7 @@ class LogicAniLife(AnimeModuleBase):
|
|||||||
"anilife_finished_insert": "[완결]",
|
"anilife_finished_insert": "[완결]",
|
||||||
"anilife_max_ffmpeg_process_count": "1",
|
"anilife_max_ffmpeg_process_count": "1",
|
||||||
"anilife_download_method": "ffmpeg", # ffmpeg or ytdlp
|
"anilife_download_method": "ffmpeg", # ffmpeg or ytdlp
|
||||||
|
"anilife_download_threads": "16", # yt-dlp/aria2c 병렬 쓰레드 수
|
||||||
"anilife_order_desc": "False",
|
"anilife_order_desc": "False",
|
||||||
"anilife_auto_start": "False",
|
"anilife_auto_start": "False",
|
||||||
"anilife_interval": "* 5 * * *",
|
"anilife_interval": "* 5 * * *",
|
||||||
@@ -164,9 +165,42 @@ class LogicAniLife(AnimeModuleBase):
|
|||||||
def __init__(self, P):
|
def __init__(self, P):
|
||||||
super(LogicAniLife, self).__init__(P, setup_default=self.db_default, name=name, first_menu='setting', scheduler_desc="애니라이프 자동 다운로드")
|
super(LogicAniLife, self).__init__(P, setup_default=self.db_default, name=name, first_menu='setting', scheduler_desc="애니라이프 자동 다운로드")
|
||||||
self.queue = None
|
self.queue = None
|
||||||
|
self.web_list_model = ModelAniLifeItem
|
||||||
self.OS_PLATFORM = platform.system()
|
self.OS_PLATFORM = platform.system()
|
||||||
default_route_socketio_module(self, attach="/search")
|
default_route_socketio_module(self, attach="/search")
|
||||||
|
|
||||||
|
def process_command(self, command, arg1, arg2, arg3, req):
|
||||||
|
try:
|
||||||
|
if command == "list":
|
||||||
|
ret = self.queue.get_entity_list() if self.queue else []
|
||||||
|
return jsonify(ret)
|
||||||
|
elif command == "stop":
|
||||||
|
entity_id = int(arg1) if arg1 else -1
|
||||||
|
result = self.queue.command("cancel", entity_id) if self.queue else {"ret": "error"}
|
||||||
|
return jsonify(result)
|
||||||
|
elif command == "remove":
|
||||||
|
entity_id = int(arg1) if arg1 else -1
|
||||||
|
result = self.queue.command("remove", entity_id) if self.queue else {"ret": "error"}
|
||||||
|
return jsonify(result)
|
||||||
|
elif command in ["reset", "delete_completed"]:
|
||||||
|
result = self.queue.command(command, 0) if self.queue else {"ret": "error"}
|
||||||
|
return jsonify(result)
|
||||||
|
elif command == "merge_subtitle":
|
||||||
|
# AniUtil already imported at module level
|
||||||
|
db_id = int(arg1)
|
||||||
|
db_item = ModelAniLifeItem.get_by_id(db_id)
|
||||||
|
if db_item and db_item.status == 'completed':
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=AniUtil.merge_subtitle, args=(self.P, db_item)).start()
|
||||||
|
return jsonify({"ret": "success", "log": "자막 합칩을 시작합니다."})
|
||||||
|
return jsonify({"ret": "fail", "log": "파일을 찾을 수 없거나 완료된 상태가 아닙니다."})
|
||||||
|
|
||||||
|
return jsonify({"ret": "fail", "log": f"Unknown command: {command}"})
|
||||||
|
except Exception as e:
|
||||||
|
self.P.logger.error(f"process_command Error: {e}")
|
||||||
|
self.P.logger.error(traceback.format_exc())
|
||||||
|
return jsonify({'ret': 'fail', 'log': str(e)})
|
||||||
|
|
||||||
# @staticmethod
|
# @staticmethod
|
||||||
def get_html(
|
def get_html(
|
||||||
self,
|
self,
|
||||||
@@ -578,11 +612,18 @@ class LogicAniLife(AnimeModuleBase):
|
|||||||
socketio.emit(
|
socketio.emit(
|
||||||
"notify", notify, namespace="/framework", broadcast=True
|
"notify", notify, namespace="/framework", broadcast=True
|
||||||
)
|
)
|
||||||
|
|
||||||
thread = threading.Thread(target=func, args=())
|
thread = threading.Thread(target=func, args=())
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
return jsonify("")
|
return jsonify("")
|
||||||
|
elif sub == "proxy_image":
|
||||||
|
image_url = request.args.get("url") or request.args.get("image_url")
|
||||||
|
return self.proxy_image(image_url)
|
||||||
|
elif sub == "entity_list":
|
||||||
|
if self.queue is not None:
|
||||||
|
return jsonify(self.queue.get_entity_list())
|
||||||
|
else:
|
||||||
|
return jsonify([])
|
||||||
elif sub == "web_list":
|
elif sub == "web_list":
|
||||||
return jsonify(ModelAniLifeItem.web_list(request))
|
return jsonify(ModelAniLifeItem.web_list(request))
|
||||||
elif sub == "db_remove":
|
elif sub == "db_remove":
|
||||||
@@ -657,48 +698,14 @@ class LogicAniLife(AnimeModuleBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"browse_dir error: {e}")
|
logger.error(f"browse_dir error: {e}")
|
||||||
return jsonify({"ret": "error", "error": str(e)}), 500
|
return jsonify({"ret": "error", "error": str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({"ret": "fail", "log": f"Unknown sub: {sub}"})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error("Exception:%s", e)
|
P.logger.error("AniLife process_ajax Exception:%s", e)
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
return jsonify({"ret": "exception", "log": str(e)})
|
||||||
|
|
||||||
def process_command(self, command, arg1, arg2, arg3, req):
|
|
||||||
ret = {"ret": "success"}
|
|
||||||
logger.debug("queue_list")
|
|
||||||
if command == "queue_list":
|
|
||||||
logger.debug(
|
|
||||||
f"self.queue.get_entity_list():: {self.queue.get_entity_list()}"
|
|
||||||
)
|
|
||||||
ret = [x for x in self.queue.get_entity_list()]
|
|
||||||
|
|
||||||
return ret
|
|
||||||
elif command == "download_program":
|
|
||||||
_pass = arg2
|
|
||||||
db_item = ModelOhli24Program.get(arg1)
|
|
||||||
if _pass == "false" and db_item != None:
|
|
||||||
ret["ret"] = "warning"
|
|
||||||
ret["msg"] = "이미 DB에 있는 항목 입니다."
|
|
||||||
elif (
|
|
||||||
_pass == "true"
|
|
||||||
and db_item != None
|
|
||||||
and ModelOhli24Program.get_by_id_in_queue(db_item.id) != None
|
|
||||||
):
|
|
||||||
ret["ret"] = "warning"
|
|
||||||
ret["msg"] = "이미 큐에 있는 항목 입니다."
|
|
||||||
else:
|
|
||||||
if db_item == None:
|
|
||||||
db_item = ModelOhli24Program(arg1, self.get_episode(arg1))
|
|
||||||
db_item.save()
|
|
||||||
db_item.init_for_queue()
|
|
||||||
self.download_queue.put(db_item)
|
|
||||||
ret["msg"] = "다운로드를 추가 하였습니다."
|
|
||||||
|
|
||||||
elif command == "list":
|
|
||||||
# Anilife 큐의 entity_list 반환 (이전: SupportFfmpeg.get_list() - 잘못된 소스)
|
|
||||||
ret = []
|
|
||||||
for entity in self.queue.entity_list:
|
|
||||||
ret.append(entity.as_dict())
|
|
||||||
|
|
||||||
return jsonify(ret)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_whitelist(*args):
|
def add_whitelist(*args):
|
||||||
@@ -765,16 +772,50 @@ class LogicAniLife(AnimeModuleBase):
|
|||||||
self.queue = FfmpegQueue(
|
self.queue = FfmpegQueue(
|
||||||
P, P.ModelSetting.get_int("anilife_max_ffmpeg_process_count"), name, self
|
P, P.ModelSetting.get_int("anilife_max_ffmpeg_process_count"), name, self
|
||||||
)
|
)
|
||||||
|
self.queue.queue_start()
|
||||||
|
|
||||||
|
# 데이터 마이그레이션/동기화: 파일명이 비어있는 항목들 처리
|
||||||
|
from framework import app
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
items = ModelAniLifeItem.get_list_uncompleted()
|
||||||
|
for item in items:
|
||||||
|
if not item.filename or item.filename == item.title:
|
||||||
|
# 임시로 Entity를 만들어 파일명 생성 로직 활용
|
||||||
|
tmp_info = item.anilife_info if item.anilife_info else {}
|
||||||
|
# dict가 아닐 경우 처리 (문자열 등)
|
||||||
|
if isinstance(tmp_info, str):
|
||||||
|
try: tmp_info = json.loads(tmp_info)
|
||||||
|
except: tmp_info = {}
|
||||||
|
|
||||||
|
tmp_entity = AniLifeQueueEntity(P, self, tmp_info)
|
||||||
|
if tmp_entity.filename:
|
||||||
|
item.filename = tmp_entity.filename
|
||||||
|
item.save()
|
||||||
|
logger.info(f"Synced filename for item {item.id}: {item.filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Data sync error: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
self.current_data = None
|
self.current_data = None
|
||||||
self.queue.queue_start()
|
self.queue.queue_start()
|
||||||
|
|
||||||
# Camoufox 미리 준비 (백그라운드에서 설치 및 바이너리 다운로드)
|
# Camoufox 미리 준비 (백그라운드에서 설치 및 바이너리 다운로드)
|
||||||
threading.Thread(target=self.ensure_camoufox_installed, daemon=True).start()
|
threading.Thread(target=self.ensure_camoufox_installed, daemon=True).start()
|
||||||
|
|
||||||
|
def db_delete(self, day):
|
||||||
|
try:
|
||||||
|
# 전체 삭제 (일수 기준 또는 전체)
|
||||||
|
return ModelAniLifeItem.delete_all()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Exception: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
def scheduler_function(self):
|
def scheduler_function(self):
|
||||||
logger.debug(f"ohli24 scheduler_function::=========================")
|
logger.debug(f"ohli24 scheduler_function::=========================")
|
||||||
|
|
||||||
content_code_list = P.ModelSetting.get_list("ohli24_auto_code_list", "|")
|
content_code_list = P.ModelSetting.get_list("anilife_auto_code_list", "|")
|
||||||
url = f'{P.ModelSetting.get("anilife_url")}/dailyani'
|
url = f'{P.ModelSetting.get("anilife_url")}/dailyani'
|
||||||
if "all" in content_code_list:
|
if "all" in content_code_list:
|
||||||
ret_data = LogicAniLife.get_auto_anime_info(self, url=url)
|
ret_data = LogicAniLife.get_auto_anime_info(self, url=url)
|
||||||
@@ -1158,9 +1199,39 @@ class LogicAniLife(AnimeModuleBase):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error(f"Exception: {str(e)}")
|
P.logger.error(f"AniLife process_ajax Error: {str(e)}")
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
return {"ret": "exception", "log": str(e)}
|
return jsonify({"ret": "exception", "log": str(e)})
|
||||||
|
|
||||||
|
def proxy_image(self, image_url):
|
||||||
|
try:
|
||||||
|
if not image_url or image_url == "None":
|
||||||
|
return ""
|
||||||
|
import requests
|
||||||
|
headers = {
|
||||||
|
'Referer': 'https://anilife.live/',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
}
|
||||||
|
res = requests.get(image_url, headers=headers, stream=True, timeout=10)
|
||||||
|
from flask import Response
|
||||||
|
return Response(res.content, mimetype=res.headers.get('content-type', 'image/jpeg'))
|
||||||
|
except Exception as e:
|
||||||
|
P.logger.error(f"AniLife proxy_image error: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def vtt_proxy(self, vtt_url):
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
headers = {
|
||||||
|
'Referer': 'https://anilife.live/',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
}
|
||||||
|
res = requests.get(vtt_url, headers=headers, timeout=10)
|
||||||
|
from flask import Response
|
||||||
|
return Response(res.text, mimetype='text/vtt')
|
||||||
|
except Exception as e:
|
||||||
|
P.logger.error(f"AniLife vtt_proxy error: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
def add(self, episode_info):
|
def add(self, episode_info):
|
||||||
@@ -1210,8 +1281,27 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
|
|||||||
self.content_title = None
|
self.content_title = None
|
||||||
self.srt_url = None
|
self.srt_url = None
|
||||||
self.headers = None
|
self.headers = None
|
||||||
# [Lazy Extraction] __init__에서는 무거운 분석을 하지 않습니다.
|
self.filename = info.get("title")
|
||||||
# self.make_episode_info()
|
self.epi_queue = info.get("ep_num")
|
||||||
|
self.content_title = info.get("title")
|
||||||
|
|
||||||
|
def get_downloader(self, video_url, output_file, callback=None, callback_function=None):
|
||||||
|
from .lib.downloader_factory import DownloaderFactory
|
||||||
|
# Anilife는 설정이 따로 없으면 기본 ytdlp 사용하거나 ffmpeg
|
||||||
|
method = self.P.ModelSetting.get("anilife_download_method") or "ffmpeg"
|
||||||
|
threads = self.P.ModelSetting.get_int("anilife_download_threads") or 16
|
||||||
|
logger.info(f"AniLife get_downloader using method: {method}, threads: {threads}")
|
||||||
|
|
||||||
|
return DownloaderFactory.get_downloader(
|
||||||
|
method=method,
|
||||||
|
video_url=video_url,
|
||||||
|
output_file=output_file,
|
||||||
|
headers=self.headers,
|
||||||
|
callback=callback,
|
||||||
|
callback_id="anilife",
|
||||||
|
threads=threads,
|
||||||
|
callback_function=callback_function
|
||||||
|
)
|
||||||
|
|
||||||
def refresh_status(self):
|
def refresh_status(self):
|
||||||
self.module_logic.socketio_callback("status", self.as_dict())
|
self.module_logic.socketio_callback("status", self.as_dict())
|
||||||
@@ -1223,17 +1313,30 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
|
|||||||
tmp["vtt"] = self.vtt
|
tmp["vtt"] = self.vtt
|
||||||
tmp["season"] = self.season
|
tmp["season"] = self.season
|
||||||
tmp["content_title"] = self.content_title
|
tmp["content_title"] = self.content_title
|
||||||
|
# 큐 리스트에서 '에피소드 제목'으로 명확히 인지되도록 함
|
||||||
|
tmp["episode_title"] = self.info.get("title")
|
||||||
tmp["anilife_info"] = self.info
|
tmp["anilife_info"] = self.info
|
||||||
tmp["epi_queue"] = self.epi_queue
|
tmp["epi_queue"] = self.epi_queue
|
||||||
|
tmp["filename"] = self.filename
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
def donwload_completed(self):
|
def donwload_completed(self):
|
||||||
db_entity = ModelAniLifeItem.get_by_anilife_id(self.info["_id"])
|
db_entity = ModelAniLifeItem.get_by_anilife_id(self.info["_id"])
|
||||||
if db_entity is not None:
|
if db_entity is not None:
|
||||||
db_entity.status = "completed"
|
db_entity.status = "completed"
|
||||||
db_entity.complated_time = datetime.now()
|
db_entity.completed_time = datetime.now()
|
||||||
|
# 메타데이터 동기화
|
||||||
|
db_entity.filename = self.filename
|
||||||
|
db_entity.save_fullpath = self.save_fullpath
|
||||||
|
db_entity.filesize = self.filesize
|
||||||
|
db_entity.duration = self.duration
|
||||||
|
db_entity.quality = self.quality
|
||||||
db_entity.save()
|
db_entity.save()
|
||||||
|
|
||||||
|
# Discord 알림 (이미 메인에서 처리될 수도 있으나 명시적으로 필요한 경우)
|
||||||
|
# if self.P.ModelSetting.get_bool('anilife_discord_notification'):
|
||||||
|
# ...
|
||||||
|
|
||||||
def prepare_extra(self):
|
def prepare_extra(self):
|
||||||
"""
|
"""
|
||||||
[Lazy Extraction] prepare_extra() replaces make_episode_info()
|
[Lazy Extraction] prepare_extra() replaces make_episode_info()
|
||||||
@@ -1305,6 +1408,10 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
|
|||||||
def log_stderr(pipe):
|
def log_stderr(pipe):
|
||||||
for line in iter(pipe.readline, ''):
|
for line in iter(pipe.readline, ''):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
|
# tqdm 진행바나 불필요한 로그는 debug 레벨로 출력하여 로그 도배 방지
|
||||||
|
if '%' in line or '|' in line or 'addon' in line.lower():
|
||||||
|
logger.debug(f"[Camoufox-Progress] {line.strip()}")
|
||||||
|
else:
|
||||||
logger.info(f"[Camoufox] {line.strip()}")
|
logger.info(f"[Camoufox] {line.strip()}")
|
||||||
|
|
||||||
stderr_thread = threading.Thread(target=log_stderr, args=(process.stderr,))
|
stderr_thread = threading.Thread(target=log_stderr, args=(process.stderr,))
|
||||||
@@ -1314,7 +1421,13 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
|
|||||||
for line in iter(process.stdout.readline, ''):
|
for line in iter(process.stdout.readline, ''):
|
||||||
stdout_data.append(line)
|
stdout_data.append(line)
|
||||||
|
|
||||||
|
try:
|
||||||
process.wait(timeout=120)
|
process.wait(timeout=120)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("Camoufox subprocess timed out (120s)")
|
||||||
|
process.kill()
|
||||||
|
return
|
||||||
|
|
||||||
stderr_thread.join(timeout=5)
|
stderr_thread.join(timeout=5)
|
||||||
|
|
||||||
stdout_full = "".join(stdout_data)
|
stdout_full = "".join(stdout_data)
|
||||||
@@ -1468,26 +1581,25 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
|
|||||||
|
|
||||||
self.epi_queue = epi_no
|
self.epi_queue = epi_no
|
||||||
|
|
||||||
self.filename = Util.change_text_for_use_filename(ret)
|
self.filename = AniUtil.change_text_for_use_filename(ret)
|
||||||
logger.info(f"Filename: {self.filename}")
|
logger.info(f"Filename: {self.filename}")
|
||||||
|
|
||||||
# anilife 전용 다운로드 경로 설정 (ohli24_download_path 대신 anilife_download_path 사용)
|
# anilife 전용 다운로드 경로 설정
|
||||||
self.savepath = P.ModelSetting.get("anilife_download_path")
|
self.savepath = P.ModelSetting.get("anilife_download_path")
|
||||||
if not self.savepath:
|
|
||||||
self.savepath = P.ModelSetting.get("ohli24_download_path")
|
|
||||||
logger.info(f"Savepath: {self.savepath}")
|
logger.info(f"Savepath: {self.savepath}")
|
||||||
|
|
||||||
if P.ModelSetting.get_bool("ohli24_auto_make_folder"):
|
if P.ModelSetting.get_bool("anilife_auto_make_folder"):
|
||||||
if self.info.get("day", "").find("완결") != -1:
|
if self.info.get("day", "").find("완결") != -1:
|
||||||
folder_name = "%s %s" % (
|
folder_name = "%s %s" % (
|
||||||
P.ModelSetting.get("ohli24_finished_insert"),
|
P.ModelSetting.get("anilife_finished_insert"),
|
||||||
self.content_title,
|
self.content_title,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
folder_name = self.content_title
|
folder_name = self.content_title
|
||||||
folder_name = Util.change_text_for_use_filename(folder_name.strip())
|
folder_name = AniUtil.change_text_for_use_filename(folder_name.strip())
|
||||||
self.savepath = os.path.join(self.savepath, folder_name)
|
self.savepath = os.path.join(self.savepath, folder_name)
|
||||||
if P.ModelSetting.get_bool("ohli24_auto_make_season_folder"):
|
|
||||||
|
if P.ModelSetting.get_bool("anilife_auto_make_season_folder"):
|
||||||
self.savepath = os.path.join(
|
self.savepath = os.path.join(
|
||||||
self.savepath, "Season %s" % int(self.season)
|
self.savepath, "Season %s" % int(self.season)
|
||||||
)
|
)
|
||||||
@@ -1547,12 +1659,17 @@ class ModelAniLifeItem(db.Model):
|
|||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
ret = {x.name: getattr(self, x.name) for x in self.__table__.columns}
|
ret = {x.name: getattr(self, x.name) for x in self.__table__.columns}
|
||||||
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S")
|
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S") if self.created_time is not None else None
|
||||||
ret["completed_time"] = (
|
ret["completed_time"] = (
|
||||||
self.completed_time.strftime("%Y-%m-%d %H:%M:%S")
|
self.completed_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if self.completed_time is not None
|
if self.completed_time is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
# 템플릿 호환용 (anilife_list.html)
|
||||||
|
ret["image_link"] = self.thumbnail
|
||||||
|
ret["ep_num"] = self.episode_no
|
||||||
|
# content_title이 없으면 제목(시리즈명)으로 활용
|
||||||
|
ret["content_title"] = self.anilife_info.get("content_title") if self.anilife_info else self.title
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@@ -1569,9 +1686,33 @@ class ModelAniLifeItem(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_by_id(cls, idx):
|
def delete_by_id(cls, idx):
|
||||||
db.session.query(cls).filter_by(id=idx).delete()
|
try:
|
||||||
|
logger.debug(f"delete_by_id: {idx} (type: {type(idx)})")
|
||||||
|
if isinstance(idx, str) and ',' in idx:
|
||||||
|
id_list = [int(x.strip()) for x in idx.split(',') if x.strip()]
|
||||||
|
logger.debug(f"Batch delete: {id_list}")
|
||||||
|
count = db.session.query(cls).filter(cls.id.in_(id_list)).delete(synchronize_session='fetch')
|
||||||
|
logger.debug(f"Deleted count: {count}")
|
||||||
|
else:
|
||||||
|
db.session.query(cls).filter_by(id=int(idx)).delete()
|
||||||
|
logger.debug(f"Single delete: {idx}")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Exception: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_all(cls):
|
||||||
|
try:
|
||||||
|
db.session.query(cls).delete()
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Exception: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def web_list(cls, req):
|
def web_list(cls, req):
|
||||||
@@ -1622,22 +1763,28 @@ class ModelAniLifeItem(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def append(cls, q):
|
def append(cls, q):
|
||||||
|
# 중복 체크
|
||||||
|
existing = cls.get_by_anilife_id(q["_id"])
|
||||||
|
if existing:
|
||||||
|
logger.debug(f"Item already exists in DB: {q['_id']}")
|
||||||
|
return existing
|
||||||
|
|
||||||
item = ModelAniLifeItem()
|
item = ModelAniLifeItem()
|
||||||
item.content_code = q["content_code"]
|
item.content_code = q["content_code"]
|
||||||
item.season = q["season"]
|
item.season = q["season"]
|
||||||
item.episode_no = q["epi_queue"]
|
item.episode_no = q.get("epi_queue")
|
||||||
item.title = q["content_title"]
|
item.title = q["content_title"]
|
||||||
item.episode_title = q["title"]
|
item.episode_title = q["title"]
|
||||||
item.ohli24_va = q["va"]
|
item.anilife_va = q.get("va")
|
||||||
item.ohli24_vi = q["_vi"]
|
item.anilife_vi = q.get("_vi")
|
||||||
item.ohli24_id = q["_id"]
|
item.anilife_id = q["_id"]
|
||||||
item.quality = q["quality"]
|
item.quality = q["quality"]
|
||||||
item.filepath = q["filepath"]
|
item.filepath = q.get("filepath")
|
||||||
item.filename = q["filename"]
|
item.filename = q.get("filename")
|
||||||
item.savepath = q["savepath"]
|
item.savepath = q.get("savepath")
|
||||||
item.video_url = q["url"]
|
item.video_url = q.get("url")
|
||||||
item.vtt_url = q["vtt"]
|
item.vtt_url = q.get("vtt")
|
||||||
item.thumbnail = q["thumbnail"]
|
item.thumbnail = q.get("thumbnail")
|
||||||
item.status = "wait"
|
item.status = "wait"
|
||||||
item.ohli24_info = q["anilife_info"]
|
item.anilife_info = q.get("anilife_info")
|
||||||
item.save()
|
item.save()
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ class AnimeModuleBase(PluginModuleBase):
|
|||||||
arg3 = request.form.get('arg3') or request.args.get('arg3')
|
arg3 = request.form.get('arg3') or request.args.get('arg3')
|
||||||
return self.process_command(command, arg1, arg2, arg3, req)
|
return self.process_command(command, arg1, arg2, arg3, req)
|
||||||
|
|
||||||
|
return jsonify({'ret': 'fail', 'log': f"Unknown sub: {sub}"})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.P.logger.error(f"AJAX Error: {e}")
|
self.P.logger.error(f"AJAX Error: {e}")
|
||||||
self.P.logger.error(traceback.format_exc())
|
self.P.logger.error(traceback.format_exc())
|
||||||
|
|||||||
@@ -1691,9 +1691,10 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
"""
|
"""
|
||||||
from .lib.downloader_factory import DownloaderFactory
|
from .lib.downloader_factory import DownloaderFactory
|
||||||
|
|
||||||
# 설정에서 다운로드 방식 읽기
|
# 설정에서 다운로드 방식 및 쓰레드 수 읽기
|
||||||
method = self.P.ModelSetting.get("linkkf_download_method") or "ytdlp"
|
method = self.P.ModelSetting.get("linkkf_download_method") or "ytdlp"
|
||||||
logger.info(f"Linkkf get_downloader using method: {method}")
|
threads = self.P.ModelSetting.get_int("linkkf_download_threads") or 16
|
||||||
|
logger.info(f"Linkkf get_downloader using method: {method}, threads: {threads}")
|
||||||
|
|
||||||
return DownloaderFactory.get_downloader(
|
return DownloaderFactory.get_downloader(
|
||||||
method=method,
|
method=method,
|
||||||
@@ -1702,6 +1703,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
callback=callback,
|
callback=callback,
|
||||||
callback_id="linkkf",
|
callback_id="linkkf",
|
||||||
|
threads=threads,
|
||||||
callback_function=callback_function
|
callback_function=callback_function
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -681,12 +681,119 @@
|
|||||||
background-color: #e0ff42;
|
background-color: #e0ff42;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== Cosmic Violet Theme (Anilife Exclusive) ========== */
|
||||||
body {
|
body {
|
||||||
font-family: NanumSquareNeo, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Noto Sans, Liberation Sans, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
font-family: 'Inter', 'Noto Sans KR', system-ui, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #4c1d95 100%) !important;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #e0e7ff;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* Search Bar Styling */
|
||||||
background-image: linear-gradient(90deg, #233f48, #6c6fa2, #768dae);
|
.input-group {
|
||||||
|
background: rgba(49, 46, 129, 0.5);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#input_search {
|
||||||
|
background: rgba(30, 27, 75, 0.7) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.2) !important;
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#input_search::placeholder {
|
||||||
|
color: #c4b5fd;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn_search {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Buttons */
|
||||||
|
#anime_category {
|
||||||
|
margin: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn {
|
||||||
|
background: rgba(49, 46, 129, 0.5) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.3) !important;
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
padding: 8px 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.3) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-success {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-grey {
|
||||||
|
background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Styling */
|
||||||
|
.card {
|
||||||
|
background: rgba(49, 46, 129, 0.5) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.15) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: rgba(167, 139, 250, 0.4) !important;
|
||||||
|
box-shadow: 0 8px 30px rgba(139, 92, 246, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
background: rgba(30, 27, 75, 0.85) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: #a78bfa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Badge */
|
||||||
|
.btn-info {
|
||||||
|
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
#spinner {
|
||||||
|
color: #a78bfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo {
|
.demo {
|
||||||
@@ -809,5 +916,21 @@
|
|||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding-top: 10px !important; }
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 50px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills .nav-link {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,305 +1,662 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #4c1d95 100%) !important;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #e0e7ff;
|
||||||
|
font-family: 'Inter', 'Noto Sans KR', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout Expansion */
|
||||||
|
#main_container, .container, .container-fluid, .content-cloak {
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding-left: 15px !important;
|
||||||
|
padding-right: 15px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-cloak { padding-top: 10px; }
|
||||||
|
|
||||||
|
/* Navigation (Tabs) Optimization */
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
background-color: rgba(30, 41, 59, 0.6) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 50rem !important;
|
||||||
|
padding: 6px !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nav.nav-pills .nav-link {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 8px 20px !important;
|
||||||
|
border-radius: 50rem !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nav.nav-pills .nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nav.nav-pills .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Container */
|
||||||
|
.search-container {
|
||||||
|
background: rgba(49, 46, 129, 0.4);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between; /* Align groups to edges */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-group-left { display: flex; gap: 8px; flex: 1; min-width: 200px; }
|
||||||
|
.search-group-right { display: flex; gap: 8px; flex: 1.5; min-width: 320px; justify-content: flex-end; }
|
||||||
|
|
||||||
|
.custom-select { min-width: 100px; flex: 1; }
|
||||||
|
.custom-input { flex: 2; min-width: 150px; }
|
||||||
|
|
||||||
|
.custom-select, .custom-input {
|
||||||
|
background-color: rgba(30, 27, 75, 0.6) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.2) !important;
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
height: 38px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
.custom-select:focus, .custom-input:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3) !important;
|
||||||
|
border-color: #8b5cf6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bootstrap Toggle Custom Styling (Match Request Page) */
|
||||||
|
.toggle.btn-success, .toggle.btn-success:hover {
|
||||||
|
background: linear-gradient(135deg, #f472b6 0%, #db2777 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(236, 72, 153, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-on.btn-success {
|
||||||
|
background: transparent !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-off.btn-secondary {
|
||||||
|
background: rgba(30, 27, 75, 0.6) !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-btn {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
height: 38px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.btn-search:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); }
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
}
|
||||||
|
.btn-reset:hover { background: rgba(139, 92, 246, 0.2); color: white !important; }
|
||||||
|
|
||||||
|
/* List Styling */
|
||||||
|
#list_div { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
background: rgba(49, 46, 129, 0.25);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex; gap: 15px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.item-row:hover {
|
||||||
|
background: rgba(49, 46, 129, 0.45);
|
||||||
|
border-color: rgba(167, 139, 250, 0.35);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-container {
|
||||||
|
width: 100px; height: 140px; flex-shrink: 0; border-radius: 8px; overflow: hidden; position: relative;
|
||||||
|
background: #1e1b4b;
|
||||||
|
}
|
||||||
|
.poster-img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s; }
|
||||||
|
.item-row:hover .poster-img { transform: scale(1.05); }
|
||||||
|
|
||||||
|
.episode-badge {
|
||||||
|
position: absolute; top: 0; left: 0;
|
||||||
|
background: rgba(139, 92, 246, 0.9);
|
||||||
|
color: white;
|
||||||
|
font-size: 10px; font-weight: 800;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
box-shadow: 1px 1px 4px rgba(0,0,0,0.3);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-size: 16px; font-weight: 700; color: #fff;
|
||||||
|
margin-bottom: 6px; line-height: 1.3;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
|
||||||
|
line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
|
||||||
|
.meta-program { font-size: 13px; color: #a78bfa; font-weight: 600; display: flex; align-items: center; gap: 4px; }
|
||||||
|
.meta-date { font-size: 12px; color: #94a3b8; }
|
||||||
|
|
||||||
|
.filename-text {
|
||||||
|
font-size: 11px; color: #94a3b8; background: rgba(0,0,0,0.2); padding: 5px 10px; border-radius: 6px;
|
||||||
|
word-break: break-all; margin-bottom: 12px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.action-btn {
|
||||||
|
padding: 6px 14px; font-size: 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
background: rgba(255,255,255,0.05); color: #c4b5fd; white-space: nowrap;
|
||||||
|
display: flex; align-items: center; gap: 6px; transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.action-btn:hover { background: rgba(139, 92, 246, 0.2); color: white; border-color: rgba(139, 92, 246, 0.4); }
|
||||||
|
.action-btn.btn-play { background: linear-gradient(135deg, #8b5cf6, #7c3aed); color: white; border: none; font-weight: 700; }
|
||||||
|
|
||||||
|
/* Modal Styling */
|
||||||
|
.modal-content {
|
||||||
|
background: #1e1b4b; border-radius: 16px; border: 1px solid rgba(167, 139, 250, 0.2);
|
||||||
|
}
|
||||||
|
.modal-header { border-bottom: 1px solid rgba(167, 139, 250, 0.1); }
|
||||||
|
.modal-title { color: #e0e7ff; }
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 11px; font-weight: 700; padding: 2px 10px; border-radius: 50rem;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.status-completed { background: rgba(34, 197, 94, 0.2); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.3); }
|
||||||
|
.status-downloading { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
|
||||||
|
.status-wait { background: rgba(148, 163, 184, 0.2); color: #94a3b8; border: 1px solid rgba(148, 163, 184, 0.3); }
|
||||||
|
.status-fail { background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
|
||||||
|
|
||||||
|
/* Checkbox Styling */
|
||||||
|
.custom-checkbox-worker {
|
||||||
|
width: 22px; height: 22px; cursor: pointer;
|
||||||
|
background: rgba(167, 139, 250, 0.1); border: 2px solid rgba(167, 139, 250, 0.3);
|
||||||
|
border-radius: 6px; position: relative; transition: all 0.2s;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.custom-checkbox-worker:hover { background: rgba(167, 139, 250, 0.2); }
|
||||||
|
.custom-checkbox-worker.checked { background: #8b5cf6; border-color: #8b5cf6; }
|
||||||
|
.custom-checkbox-worker.checked::after {
|
||||||
|
content: '\f00c'; font-family: 'FontAwesome'; color: white; font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-select-container {
|
||||||
|
display: flex; align-items: center; gap: 8px; font-size: 13px; color: #94a3b8;
|
||||||
|
cursor: pointer; padding: 4px 10px; border-radius: 8px; transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.all-select-container:hover { background: rgba(255,255,255,0.05); color: #fff; }
|
||||||
|
|
||||||
|
.btn-delete-selected {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #fca5a5 !important;
|
||||||
|
}
|
||||||
|
.btn-delete-selected:hover { background: rgba(239, 68, 68, 0.2); color: #fff !important; }
|
||||||
|
|
||||||
|
/* Smooth Load */
|
||||||
|
.content-cloak, #menu_page_div { opacity: 0; transition: opacity 0.5s ease-out; }
|
||||||
|
.content-cloak.visible, #menu_page_div.visible { opacity: 1; }
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 50px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills .nav-link {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Container Mobile Fix */
|
||||||
|
.search-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
.search-group-left, .search-group-right {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: unset !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
.search-group-right {
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Size Reduction */
|
||||||
|
.search-group-right .btn {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
.btn-reset {
|
||||||
|
padding: 6px 8px !important;
|
||||||
|
}
|
||||||
|
.btn-reset i + span,
|
||||||
|
.btn-reset::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Overflow Prevention */
|
||||||
|
.content-cloak, #form_search, #list_div, .item-row {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Item Row Fixes */
|
||||||
|
.item-row {
|
||||||
|
padding: 8px !important;
|
||||||
|
margin: 4px 0 !important;
|
||||||
|
}
|
||||||
|
.info-container {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
.item-title, .filename-text, .meta-program {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle and Action Buttons */
|
||||||
|
.item-actions {
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Notify Styling for Mobile */
|
||||||
|
.bootstrap-notify-container,
|
||||||
|
[data-notify="container"] {
|
||||||
|
max-width: 90vw !important;
|
||||||
|
width: auto !important;
|
||||||
|
right: 5vw !important;
|
||||||
|
left: 5vw !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
background: rgba(30, 27, 75, 0.95) !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.3) !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4) !important;
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-success {
|
||||||
|
border-color: rgba(16, 185, 129, 0.4) !important;
|
||||||
|
background: rgba(6, 78, 59, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-warning {
|
||||||
|
border-color: rgba(245, 158, 11, 0.4) !important;
|
||||||
|
background: rgba(120, 53, 15, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-danger {
|
||||||
|
border-color: rgba(239, 68, 68, 0.4) !important;
|
||||||
|
background: rgba(127, 29, 29, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="message"] {
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
}
|
||||||
|
[data-notify="title"] {
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="content-cloak">
|
<div class="content-cloak">
|
||||||
<form id="form_search" class="form-inline" style="text-align:left">
|
<form id="form_search" class="form-inline" style="text-align:left; width:100%;">
|
||||||
<div class="container-fluid">
|
<div class="search-container">
|
||||||
<div class="row show-grid">
|
<div class="search-group-left">
|
||||||
<span class="col-md-4">
|
<select id="order" name="order" class="form-control custom-select">
|
||||||
<select id="order" name="order" class="form-control form-control-sm">
|
|
||||||
<option value="desc">최근순</option>
|
<option value="desc">최근순</option>
|
||||||
<option value="asc">오래된순</option>
|
<option value="asc">오래된 순</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="option" name="option" class="form-control form-control-sm">
|
<select id="option" name="option" class="form-control custom-select">
|
||||||
<option value="all">전체</option>
|
<option value="all">전체</option>
|
||||||
<option value="completed">완료</option>
|
<option value="completed">완료</option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</div>
|
||||||
<span class="col-md-8">
|
<div class="search-group-right">
|
||||||
<input id="search_word" name="search_word" class="form-control form-control-sm w-75" type="text" placeholder="" aria-label="Search">
|
<input id="search_word" name="search_word" class="form-control custom-input" type="text" placeholder="애니라이프 검색..." aria-label="Search">
|
||||||
<button id="search" class="btn btn-sm btn-outline-success">검색</button>
|
<button id="search" class="btn custom-btn btn-search"><i class="fa fa-search"></i> 검색</button>
|
||||||
<button id="reset_btn" class="btn btn-sm btn-outline-success">리셋</button>
|
<button id="delete_selected_btn" class="btn custom-btn btn-delete-selected" style="display:none;"><i class="fa fa-trash"></i> 선택 삭제</button>
|
||||||
</span>
|
<button id="reset_btn" class="btn custom-btn btn-reset"><i class="fa fa-refresh"></i> 초기화</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-start align-items:center mb-3 px-1">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<input type="checkbox" id="all_check_box" data-toggle="toggle" data-on="전체 선택" data-off="전체 선택" data-onstyle="success" data-offstyle="secondary" data-size="small">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id='page1'></div>
|
<div id='page1'></div>
|
||||||
{{ macros.m_hr_head_top() }}
|
|
||||||
{{ macros.m_row_start('0') }}
|
|
||||||
{{ macros.m_col(2, macros.m_strong('Poster')) }}
|
|
||||||
{{ macros.m_col(10, macros.m_strong('Info')) }}
|
|
||||||
{{ macros.m_row_end() }}
|
|
||||||
{{ macros.m_hr_head_bottom() }}
|
|
||||||
<div id="list_div"></div>
|
<div id="list_div"></div>
|
||||||
<div id='page2'></div>
|
<div id='page2'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DB Reset Confirmation Modal -->
|
||||||
|
<div class="modal fade animate__animated animate__fadeIn" id="db_reset_modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content" style="border-color: rgba(239, 68, 68, 0.3);">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title font-weight-bold text-danger"><i class="fa fa-trash mr-2"></i> DB 초기화 확인</h5>
|
||||||
|
<button type="button" class="close text-white" data-dismiss="modal"><span>×</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
<p class="mb-0" style="font-size: 1.1rem; color: #fecaca;">정말로 **모든** 다운로드 목록을 삭제하시겠습니까?</p>
|
||||||
|
<small class="text-muted">이 작업은 DB의 모든 기록을 삭제하며 복구할 수 없습니다.</small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="border-top: 1px solid rgba(167, 139, 250, 0.1);">
|
||||||
|
<button type="button" class="btn btn-secondary custom-btn" data-dismiss="modal" style="background: rgba(255,255,255,0.1); border: none;">취소</button>
|
||||||
|
<button type="button" id="confirm_db_reset_btn" class="btn btn-danger custom-btn" style="background: linear-gradient(135deg, #ef4444, #b91c1c); border: none;">전체 삭제 실행</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div class="modal fade animate__animated animate__fadeIn" id="delete_modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title font-weight-bold"><i class="fa fa-warning text-warning mr-2"></i> 삭제 확인</h5>
|
||||||
|
<button type="button" class="close text-white" data-dismiss="modal"><span>×</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
<p id="delete_modal_body_text" class="mb-0" style="font-size: 1.1rem; color: #cbd5e1;">정말로 이 항목을 삭제하시겠습니까?</p>
|
||||||
|
<small class="text-muted">이 작업은 되돌릴 수 없으며 원본 파일은 삭제되지 않습니다.</small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="border-top: 1px solid rgba(167, 139, 250, 0.1);">
|
||||||
|
<button type="button" class="btn btn-secondary custom-btn" data-dismiss="modal" style="background: rgba(255,255,255,0.1); border: none;">취소</button>
|
||||||
|
<button type="button" id="confirm_delete_btn" class="btn btn-danger custom-btn" style="background: linear-gradient(135deg, #ef4444, #dc2626); border: none;">삭제 실행</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('.static', filename='js/sjva_global1.js') }}"></script>
|
||||||
|
<script src="{{ url_for('.static', filename='js/sjva_ui14.js') }}"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var package_name = "{{arg['package_name']}}";
|
var package_name = "{{arg['package_name']}}";
|
||||||
var sub = "{{arg['sub']}}";
|
var sub = "{{arg['sub']}}";
|
||||||
var current_data = null;
|
var current_data = null;
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
|
setTimeout(function() { $('.content-cloak, #menu_page_div').addClass('visible'); }, 100);
|
||||||
global_sub_request_search('1');
|
global_sub_request_search('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#search").click(function(e) {
|
function global_sub_request_search(page, move_top = true) {
|
||||||
e.preventDefault();
|
var formData = get_formdata('#form_search')
|
||||||
global_sub_request_search('1');
|
formData += '&page=' + page;
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#page', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
global_sub_request_search($(this).data('page'));
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#reset_btn").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
document.getElementById("order").value = 'desc';
|
|
||||||
document.getElementById("option").value = 'all';
|
|
||||||
document.getElementById("search_word").value = '';
|
|
||||||
global_sub_request_search('1')
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#json_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var id = $(this).data('id');
|
|
||||||
for (i in current_data.list) {
|
|
||||||
if (current_data.list[i].id == id) {
|
|
||||||
m_modal(current_data.list[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#self_search_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var search_word = $(this).data('title');
|
|
||||||
document.getElementById("search_word").value = search_word;
|
|
||||||
global_sub_request_search('1')
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#remove_btn', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
id = $(this).data('id');
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/'+package_name+'/ajax/'+sub+ '/db_remove',
|
url: '/' + package_name + '/ajax/' + sub + '/web_list',
|
||||||
type: "POST",
|
type: "POST",
|
||||||
cache: false,
|
cache: false,
|
||||||
data: {id:id},
|
data: formData,
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
current_data = data;
|
||||||
|
if (move_top) window.scrollTo(0,0);
|
||||||
|
make_list(data.list);
|
||||||
|
make_page_html(data.paging);
|
||||||
|
$('#all_check_box').bootstrapToggle('off');
|
||||||
|
update_batch_btn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#search").click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
global_sub_request_search('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
$("body").on('click', '#page', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
global_sub_request_search($(this).data('page'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#reset_btn").click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// search_reset 기능 (필터 초기화)
|
||||||
|
$('#order').val('desc');
|
||||||
|
$('#option').val('all');
|
||||||
|
$('#search_word').val('');
|
||||||
|
global_sub_request_search('1');
|
||||||
|
|
||||||
|
// 사용자 요청에 따라 '초기화' 버튼이 DB 전체 삭제 기능도 포함하도록 모달 띄움
|
||||||
|
$('#db_reset_modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#confirm_db_reset_btn').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#db_reset_modal').modal('hide');
|
||||||
|
$.ajax({
|
||||||
|
url: '/' + package_name + '/ajax/' + sub + '/db_delete',
|
||||||
|
type: "POST",
|
||||||
|
cache: false,
|
||||||
|
data: {day: 'all'},
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$.notify('<strong>삭제되었습니다.</strong>', {
|
$.notify('DB를 초기화하였습니다.', {type: 'success'});
|
||||||
type: 'success'
|
global_sub_request_search('1');
|
||||||
});
|
|
||||||
global_sub_request_search(current_data.paging.current_page, false)
|
|
||||||
} else {
|
} else {
|
||||||
$.notify('<strong>삭제 실패</strong>', {
|
$.notify('초기화 실패', {type: 'warning'});
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("body").on('click', '#request_btn', function(e){
|
function make_list(data) {
|
||||||
e.preventDefault();
|
|
||||||
var content_code = $(this).data('content_code');
|
|
||||||
$(location).attr('href', '/' + package_name + '/' + sub + '/request?content_code=' + content_code)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function make_list(data) {
|
|
||||||
//console.log(data)
|
|
||||||
str = '';
|
str = '';
|
||||||
for (i in data) {
|
for (i in data) {
|
||||||
//console.log(data[i])
|
str += '<div class="item-row animate__animated animate__fadeInUp" style="animation-delay: ' + (i * 0.05) + 's;">';
|
||||||
str += m_row_start();
|
|
||||||
str += m_col(1, data[i].id);
|
// Poster
|
||||||
tmp = (data[i].status == 'completed') ? '완료' : '미완료';
|
str += '<div class="poster-container">';
|
||||||
str += m_col(1, tmp);
|
if (data[i].image_link) {
|
||||||
tmp = data[i].created_time + '(추가)';
|
// Use proxy to bypass Referer restriction
|
||||||
if (data[i].completed_time != null)
|
var proxy_url = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(data[i].image_link);
|
||||||
tmp += data[i].completed_time + '(완료)';
|
str += '<img src="' + proxy_url + '" class="poster-img" loading="lazy" onerror="this.src=\'https://placehold.co/100x140?text=No+Image\'">';
|
||||||
str += m_col(3, tmp)
|
|
||||||
tmp = data[i].savepath + '<br>' + data[i].filename + '<br><br>';
|
|
||||||
tmp2 = m_button('json_btn', 'JSON', [{'key':'id', 'value':data[i].id}]);
|
|
||||||
tmp2 += m_button('request_btn', '작품 검색', [{'key':'content_code', 'value':data[i].content_code}]);
|
|
||||||
tmp2 += m_button('self_search_btn', '목록 검색', [{'key':'title', 'value':data[i].title}]);
|
|
||||||
tmp2 += m_button('remove_btn', '삭제', [{'key':'id', 'value':data[i].id}]);
|
|
||||||
tmp += m_button_group(tmp2)
|
|
||||||
str += m_col(7, tmp)
|
|
||||||
str += m_row_end();
|
|
||||||
if (i != data.length -1) str += m_hr();
|
|
||||||
}
|
}
|
||||||
document.getElementById("list_div").innerHTML = str;
|
str += '<div class="episode-badge">' + (data[i].episode_no || '?') + '화</div>';
|
||||||
}
|
str += '</div>';
|
||||||
|
|
||||||
|
// Info
|
||||||
|
str += '<div class="info-container">';
|
||||||
|
str += '<div class="item-title-row" style="display:flex; justify-content:space-between; align-items:center;">';
|
||||||
|
// Use episode_title if available, fallback to title
|
||||||
|
var display_title = data[i].episode_title || data[i].title;
|
||||||
|
str += '<div class="item-title" title="' + display_title + '">' + display_title + '</div>';
|
||||||
|
|
||||||
|
var status_cls = 'status-wait';
|
||||||
|
var status_kor = '대기';
|
||||||
|
if (data[i].status == 'completed') { status_cls = 'status-completed'; status_kor = '완료'; }
|
||||||
|
else if (data[i].status == 'downloading') { status_cls = 'status-downloading'; status_kor = '다운로드 중'; }
|
||||||
|
else if (data[i].status == 'fail') { status_cls = 'status-fail'; status_kor = '실패'; }
|
||||||
|
|
||||||
|
str += '<span class="status-badge ' + status_cls + '">' + status_kor + '</span>';
|
||||||
|
str += '</div>';
|
||||||
|
|
||||||
|
str += '<div class="item-meta-row">';
|
||||||
|
// Show Series Title from content_title or title
|
||||||
|
var series_title = data[i].content_title || data[i].title;
|
||||||
|
str += '<span class="meta-program" title="' + series_title + '"><i class="fa fa-tv"></i> ' + series_title + '</span>';
|
||||||
|
str += '<span class="meta-date"><i class="fa fa-clock-o"></i> ' + data[i].created_time + '</span>';
|
||||||
|
str += '</div>';
|
||||||
|
|
||||||
|
str += '<div class="filename-text" title="' + data[i].filename + '">';
|
||||||
|
str += '<i class="fa fa-file-movie-o mr-1"></i> ' + (data[i].filename || '파일명 생성 전');
|
||||||
|
str += '</div>';
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
str += '<div class="item-actions">';
|
||||||
|
str += '<input type="checkbox" class="item-check-box" data-id="' + data[i].id + '" data-toggle="toggle" data-on="선택" data-off="-" data-onstyle="success" data-offstyle="secondary" data-size="small">';
|
||||||
|
if (data[i].status == 'completed') {
|
||||||
|
str += '<button id="merge_subtitle_btn" class="action-btn" data-id="' + data[i].id + '"><i class="fa fa-file-text-o"></i> 자막합침</button>';
|
||||||
|
}
|
||||||
|
str += '<button id="remove_btn" class="action-btn" data-id="' + data[i].id + '"><i class="fa fa-trash"></i> 삭제</button>';
|
||||||
|
str += '</div>';
|
||||||
|
|
||||||
|
str += '</div>'; // End Info
|
||||||
|
str += '</div>'; // End Row
|
||||||
|
}
|
||||||
|
if (str == '') str = '<div class="text-center p-5"><h4>결과가 없습니다.</h4></div>';
|
||||||
|
$("#list_div").html(str);
|
||||||
|
$('.item-check-box').bootstrapToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
var delete_id = null;
|
||||||
|
$("body").on('click', '#remove_btn', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
delete_id = $(this).data('id');
|
||||||
|
$('#delete_modal_body_text').html('정말로 이 항목을 삭제하시겠습니까?');
|
||||||
|
$('#delete_modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Batch Selection Logic (Bootstrap Toggle)
|
||||||
|
$("body").on('change', '.item-check-box', function(e) {
|
||||||
|
update_batch_btn();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#all_check_box').change(function() {
|
||||||
|
var is_checked = $(this).prop('checked');
|
||||||
|
$('.item-check-box').bootstrapToggle(is_checked ? 'on' : 'off');
|
||||||
|
update_batch_btn();
|
||||||
|
});
|
||||||
|
|
||||||
|
function update_batch_btn() {
|
||||||
|
var count = $('.item-check-box:checked').length;
|
||||||
|
if (count > 0) {
|
||||||
|
$('#delete_selected_btn').show().html('<i class="fa fa-trash"></i> 선택 삭제 (' + count + ')');
|
||||||
|
} else {
|
||||||
|
$('#delete_selected_btn').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#delete_selected_btn").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var ids = [];
|
||||||
|
$('.item-check-box:checked').each(function() {
|
||||||
|
ids.push($(this).data('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ids.length > 0) {
|
||||||
|
delete_id = ids.join(',');
|
||||||
|
$('#delete_modal_body_text').html('선택한 <b>' + ids.length + '개</b>의 항목을 정말로 삭제하시겠습니까?');
|
||||||
|
$('#delete_modal').modal('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#confirm_delete_btn').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#delete_modal').modal('hide');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/' + package_name + '/ajax/' + sub + '/db_delete_item',
|
||||||
|
type: "POST",
|
||||||
|
cache: false,
|
||||||
|
data: {db_id: delete_id},
|
||||||
|
dataType: "json",
|
||||||
|
success: function (ret) {
|
||||||
|
if (ret) {
|
||||||
|
$.notify('삭제하였습니다.', {type: 'success'});
|
||||||
|
$('#all_check_box').bootstrapToggle('off');
|
||||||
|
update_batch_btn();
|
||||||
|
global_sub_request_search(current_data.paging.current_page, false);
|
||||||
|
} else {
|
||||||
|
$.notify('삭제 실패', {type: 'warning'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("body").on('click', '#merge_subtitle_btn', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var id = $(this).data('id');
|
||||||
|
$.ajax({
|
||||||
|
url: '/' + package_name + '/ajax/' + sub + '/command',
|
||||||
|
type: "POST",
|
||||||
|
cache: false,
|
||||||
|
data: {command: 'merge_subtitle', arg1: id},
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
if (data.ret == 'success') {
|
||||||
|
$.notify('자막 합성이 백그라운드로 시작되었습니다.', {type: 'success'});
|
||||||
|
} else {
|
||||||
|
$.notify('작업 실패: ' + data.log, {type: 'warning'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
/*height: 100vh;*/
|
|
||||||
/*display: flex;*/
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-size: 300% 300%;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
-45deg,
|
|
||||||
rgba(59,173,227,1) 0%,
|
|
||||||
rgba(87,111,230,1) 25%,
|
|
||||||
rgba(152,68,183,1) 51%,
|
|
||||||
rgba(255,53,127,1) 100%
|
|
||||||
);
|
|
||||||
animation: AnimateBG 20s ease infinite;
|
|
||||||
}
|
|
||||||
#main_container {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes AnimateBG {
|
|
||||||
0%{background-position:0% 50%}
|
|
||||||
50%{background-position:100% 50%}
|
|
||||||
100%{background-position:0% 50%}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation Menu Override */
|
|
||||||
ul.nav.nav-pills.bg-light {
|
|
||||||
background-color: rgba(30, 41, 59, 0.6) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 50rem !important;
|
|
||||||
padding: 6px !important;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
|
|
||||||
display: inline-flex !important;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
width: auto !important;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-item {
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-link {
|
|
||||||
border-radius: 50rem !important;
|
|
||||||
padding: 8px 20px !important;
|
|
||||||
color: #94a3b8 !important;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-link:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff !important;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-link.active {
|
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Smooth Load Transition */
|
|
||||||
.content-cloak,
|
|
||||||
#menu_module_div,
|
|
||||||
#menu_page_div {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Staggered Delays for Natural Top-Down Flow */
|
|
||||||
#menu_module_div.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 0ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu_page_div.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 150ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-cloak.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation Menu Override (Top Sub-menu) */
|
|
||||||
ul.nav.nav-pills.bg-light {
|
|
||||||
background-color: rgba(30, 41, 59, 0.6) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 50rem !important;
|
|
||||||
padding: 6px !important;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
|
|
||||||
display: inline-flex !important;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
width: auto !important;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-item { margin: 0 2px; }
|
|
||||||
ul.nav.nav-pills .nav-link {
|
|
||||||
border-radius: 50rem !important;
|
|
||||||
padding: 8px 20px !important;
|
|
||||||
color: #94a3b8 !important;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
ul.nav.nav-pills .nav-link:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff !important;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
ul.nav.nav-pills .nav-link.active {
|
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function(){
|
|
||||||
// Smooth Load Trigger
|
|
||||||
setTimeout(function() {
|
|
||||||
$('.content-cloak, #menu_module_div, #menu_page_div').addClass('visible');
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
/* Mobile Margin Fix */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body { overflow-x: hidden !important; padding: 0 !important; margin: 0 !important; }
|
|
||||||
.container, .container-fluid, .row, form, #program_list, #program_auto_form, #episode_list, .queue-container, #yommi_wrapper, #main_container {
|
|
||||||
width: 100% !important; max-width: 100% !important;
|
|
||||||
padding-left: 4px !important; padding-right: 4px !important;
|
|
||||||
margin-left: 0 !important; margin-right: 0 !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
.form-group, .form-inline, [class*="col-"] {
|
|
||||||
flex: 0 0 100% !important; max-width: 100% !important; width: 100% !important;
|
|
||||||
padding-left: 0 !important; padding-right: 0 !important;
|
|
||||||
}
|
|
||||||
.row { margin-left: 0 !important; margin-right: 0 !important; }
|
|
||||||
.card, .card.p-4, .card.p-lg-5, .card.border-light {
|
|
||||||
width: calc(100% - 8px) !important; max-width: 100% !important;
|
|
||||||
padding: 8px !important; margin: 4px !important;
|
|
||||||
border-radius: 12px !important; box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
white-space: normal !important; text-align: left !important;
|
|
||||||
line-height: 1.4 !important; height: auto !important; display: inline-block !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -2,8 +2,15 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="content-cloak">
|
<div class="content-cloak">
|
||||||
{{ macros.m_button_group([['reset_btn', '초기화'], ['delete_completed_btn', '완료 목록 삭제'], ['go_ffmpeg_btn', 'Go FFMPEG']]) }}
|
<div class="d-flex justify-content-start align-items-center gap-2 mb-4">
|
||||||
|
<button id="reset_btn" class="btn custom-btn btn-reset-queue"><i class="fa fa-refresh mr-2"></i> 초기화</button>
|
||||||
|
<button id="delete_completed_btn" class="btn custom-btn btn-delete-completed"><i class="fa fa-trash-o mr-2"></i> 완료 목록 삭제</button>
|
||||||
|
<button id="go_ffmpeg_btn" class="btn custom-btn btn-ffmpeg"><i class="fa fa-film mr-2"></i> Go FFMPEG</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id='page1'></div>
|
||||||
|
<!-- Desktop Table View -->
|
||||||
|
<div class="table-responsive d-none d-md-block">
|
||||||
<table id="result_table" class="table table-sm tableRowHover">
|
<table id="result_table" class="table table-sm tableRowHover">
|
||||||
<thead class="thead-dark">
|
<thead class="thead-dark">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -22,6 +29,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="list"></tbody>
|
<tbody id="list"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Card View -->
|
||||||
|
<div id="list_mobile" class="d-md-none"></div>
|
||||||
|
|
||||||
|
<div id='page2'></div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const package_name = "{{arg['package_name'] }}";
|
const package_name = "{{arg['package_name'] }}";
|
||||||
@@ -36,21 +49,30 @@
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
global: !silent,
|
global: !silent,
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
// entity_list 응답을 처리
|
|
||||||
current_data = data;
|
current_data = data;
|
||||||
|
|
||||||
// 목록 개수가 변했거나 데이터가 없을 때만 전체 갱신 (반짝임 방지)
|
|
||||||
const list_body = $("#list");
|
const list_body = $("#list");
|
||||||
|
const list_mobile = $("#list_mobile");
|
||||||
|
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
list_body.html("<tr><td colspan='11'><h4>작업이 없습니다.</h4><td><tr>");
|
list_body.html("<tr><td colspan='11'><h4>작업이 없습니다.</h4><td><tr>");
|
||||||
} else if (list_body.children().length !== data.length * 2) { // make_item이 행 2개를 생성하므로
|
list_mobile.html("<div class='text-center p-5'><h4>작업이 없습니다.</h4></div>");
|
||||||
str = ''
|
|
||||||
for (i in data) {
|
|
||||||
str += make_item(data[i]);
|
|
||||||
}
|
|
||||||
list_body.html(str);
|
|
||||||
} else {
|
} else {
|
||||||
// 개수가 같으면 각 항목의 상태만 보강 업데이트
|
// Table View update
|
||||||
|
if (list_body.children().length !== data.length * 2) {
|
||||||
|
var str_table = '';
|
||||||
|
for (i in data) str_table += make_item(data[i]);
|
||||||
|
list_body.html(str_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile Card View update
|
||||||
|
if (list_mobile.children().length !== data.length) {
|
||||||
|
var str_mobile = '';
|
||||||
|
for (i in data) str_mobile += make_item_mobile(data[i]);
|
||||||
|
list_mobile.html(str_mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status updates for both
|
||||||
for (i in data) {
|
for (i in data) {
|
||||||
status_html(data[i]);
|
status_html(data[i]);
|
||||||
}
|
}
|
||||||
@@ -99,12 +121,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('add', function (data) {
|
socket.on('add', function (data) {
|
||||||
str = make_item(data);
|
|
||||||
if (current_data == null || current_data.length == 0) {
|
if (current_data == null || current_data.length == 0) {
|
||||||
current_data = Array();
|
current_data = Array();
|
||||||
$("#list").html(str);
|
$("#list").html(make_item(data));
|
||||||
|
$("#list_mobile").html(make_item_mobile(data));
|
||||||
} else {
|
} else {
|
||||||
$("#list").html($("#list").html() + str);
|
$("#list").append(make_item(data));
|
||||||
|
$("#list_mobile").append(make_item_mobile(data));
|
||||||
}
|
}
|
||||||
current_data.push(data);
|
current_data.push(data);
|
||||||
});
|
});
|
||||||
@@ -121,16 +144,20 @@
|
|||||||
globalSendCommand('list', null, null, null, function (data) {
|
globalSendCommand('list', null, null, null, function (data) {
|
||||||
current_data = data;
|
current_data = data;
|
||||||
$("#list").html('');
|
$("#list").html('');
|
||||||
// console.log(data)
|
$("#list_mobile").html('');
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
str = "<tr><td colspan='10'><h4>작업이 없습니다.</h4><td><tr>";
|
$("#list").html("<tr><td colspan='10'><h4>작업이 없습니다.</h4><td><tr>");
|
||||||
|
$("#list_mobile").html("<div class='text-center p-5'><h4>작업이 없습니다.</h4></div>");
|
||||||
} else {
|
} else {
|
||||||
str = ''
|
var str_table = '';
|
||||||
|
var str_mobile = '';
|
||||||
for (i in data) {
|
for (i in data) {
|
||||||
str += make_item(data[i]);
|
str_table += make_item(data[i]);
|
||||||
|
str_mobile += make_item_mobile(data[i]);
|
||||||
}
|
}
|
||||||
|
$("#list").html(str_table);
|
||||||
|
$("#list_mobile").html(str_mobile);
|
||||||
}
|
}
|
||||||
$("#list").html(str);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -140,15 +167,38 @@
|
|||||||
$("body").on('click', '#stop_btn', function (e) {
|
$("body").on('click', '#stop_btn', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
globalSendCommand('stop', $(this).data('idx'), null, null, function (ret) {
|
var idx = $(this).data('idx');
|
||||||
refresh_item(ret.data);
|
globalSendCommand('stop', idx, null, null, function (ret) {
|
||||||
|
// refresh_item is legacy, on_start will handle it or socket will
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function refresh_item(data) {
|
function make_item_mobile(data) {
|
||||||
$('#tr1_' + data.idx).html(make_item1(data));
|
var str = '';
|
||||||
$('#collapse_' + data.idx).html(make_item2(data));
|
str += '<div class="queue-card mb-3" id="card_' + data.idx + '">';
|
||||||
|
str += ' <div class="card-header-flex">';
|
||||||
|
str += ' <span class="card-idx">#' + data.idx + '</span>';
|
||||||
|
str += ' <span id="card_status_kor_' + data.idx + '" class="status-badge status-' + data.status_str.toLowerCase() + '">' + data.status_kor + '</span>';
|
||||||
|
str += ' </div>';
|
||||||
|
str += ' <div class="card-content">';
|
||||||
|
str += ' <div class="card-filename">' + data.filename + '</div>';
|
||||||
|
str += ' <div class="progress mt-3 mb-2" style="height: 10px;">';
|
||||||
|
str += ' <div id="card_progress_bar_' + data.idx + '" class="progress-bar" style="width:' + data.percent + '%;"></div>';
|
||||||
|
str += ' </div>';
|
||||||
|
str += ' <div class="card-meta">';
|
||||||
|
str += ' <div class="meta-item"><i class="fa fa-clock-o"></i> <span id="card_download_time_' + data.idx + '">' + data.download_time + '</span></div>';
|
||||||
|
str += ' <div class="meta-item"><i class="fa fa-bolt"></i> <span id="card_current_speed_' + data.idx + '">' + data.current_speed + '</span></div>';
|
||||||
|
str += ' <div class="meta-item"><i class="fa fa-exclamation-triangle"></i> <span id="card_current_pf_count_' + data.idx + '">' + data.current_pf_count + '</span></div>';
|
||||||
|
str += ' </div>';
|
||||||
|
str += ' </div>';
|
||||||
|
str += ' <div id="card_button_' + data.idx + '" class="card-footer-actions">';
|
||||||
|
if (data.status_str == 'DOWNLOADING') {
|
||||||
|
str += ' <button id="stop_btn" class="action-btn btn-stop w-100" data-idx="' + data.idx + '"><i class="fa fa-stop mr-1"></i> 서비스 중지</button>';
|
||||||
|
}
|
||||||
|
str += ' </div>';
|
||||||
|
str += '</div>';
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function make_item(data) {
|
function make_item(data) {
|
||||||
@@ -214,17 +264,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function button_html(data) {
|
function button_html(data) {
|
||||||
//console.log(data)
|
var btn_str = '';
|
||||||
str = '';
|
var btn_mobile_str = '';
|
||||||
if (data.status_str == 'DOWNLOADING') {
|
if (data.status_str == 'DOWNLOADING') {
|
||||||
str = j_button('stop_btn', '중지', {'idx': data.idx}, 'danger', false, false);
|
btn_str = '<button id="stop_btn" class="action-btn btn-stop" data-idx="' + data.idx + '"><i class="fa fa-stop mr-1"></i> 중지</button>';
|
||||||
|
btn_mobile_str = '<button id="stop_btn" class="action-btn btn-stop w-100" data-idx="' + data.idx + '"><i class="fa fa-stop mr-1"></i> 서비스 중지</button>';
|
||||||
}
|
}
|
||||||
$("#button_" + data.idx).html(str);
|
$("#button_" + data.idx).html(btn_str);
|
||||||
|
$("#card_button_" + data.idx).html(btn_mobile_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
function status_html(data) {
|
function status_html(data) {
|
||||||
|
// Table Update
|
||||||
var progress = document.getElementById("progress_" + data.idx);
|
var progress = document.getElementById("progress_" + data.idx);
|
||||||
if (!progress) return;
|
if (progress) {
|
||||||
progress.style.width = data.percent + '%';
|
progress.style.width = data.percent + '%';
|
||||||
progress.innerHTML = data.percent + '%';
|
progress.innerHTML = data.percent + '%';
|
||||||
progress.style.visibility = 'visible';
|
progress.style.visibility = 'visible';
|
||||||
@@ -233,6 +286,29 @@
|
|||||||
document.getElementById("current_speed_" + data.idx).innerHTML = data.current_speed;
|
document.getElementById("current_speed_" + data.idx).innerHTML = data.current_speed;
|
||||||
document.getElementById("download_time_" + data.idx).innerHTML = data.download_time;
|
document.getElementById("download_time_" + data.idx).innerHTML = data.download_time;
|
||||||
document.getElementById("detail_" + data.idx).innerHTML = get_detail(data);
|
document.getElementById("detail_" + data.idx).innerHTML = get_detail(data);
|
||||||
|
button_html(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile Card Update
|
||||||
|
var card_progress = document.getElementById("card_progress_bar_" + data.idx);
|
||||||
|
if (card_progress) {
|
||||||
|
card_progress.style.width = data.percent + '%';
|
||||||
|
var status_badge = document.getElementById("card_status_kor_" + data.idx);
|
||||||
|
status_badge.innerHTML = data.status_kor;
|
||||||
|
status_badge.className = 'status-badge status-' + data.status_str.toLowerCase();
|
||||||
|
|
||||||
|
document.getElementById("card_download_time_" + data.idx).innerHTML = data.download_time;
|
||||||
|
document.getElementById("card_current_speed_" + data.idx).innerHTML = data.current_speed;
|
||||||
|
document.getElementById("card_current_pf_count_" + data.idx).innerHTML = data.current_pf_count;
|
||||||
|
|
||||||
|
// Card Button Update
|
||||||
|
var card_btn_container = document.getElementById("card_button_" + data.idx);
|
||||||
|
if (data.status_str == 'DOWNLOADING') {
|
||||||
|
card_btn_container.innerHTML = '<button id="stop_btn" class="action-btn btn-stop w-100" data-idx="' + data.idx + '"><i class="fa fa-stop mr-1"></i> 서비스 중지</button>';
|
||||||
|
} else {
|
||||||
|
card_btn_container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("body").on('click', '#reset_btn', function (e) {
|
$("body").on('click', '#reset_btn', function (e) {
|
||||||
@@ -242,6 +318,17 @@
|
|||||||
queue_command(send_data)
|
queue_command(send_data)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("body").on('click', '#delete_completed_btn', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
send_data = {'command': 'delete_completed', 'entity_id': -1}
|
||||||
|
queue_command(send_data)
|
||||||
|
});
|
||||||
|
|
||||||
|
$("body").on('click', '#go_ffmpeg_btn', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.href = '/ffmpeg/list';
|
||||||
|
});
|
||||||
|
|
||||||
function queue_command(data) {
|
function queue_command(data) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/' + package_name + '/ajax/' + sub + '/queue_command',
|
url: '/' + package_name + '/ajax/' + sub + '/queue_command',
|
||||||
@@ -252,17 +339,156 @@
|
|||||||
success: function (ret) {
|
success: function (ret) {
|
||||||
if (ret.ret == 'notify') {
|
if (ret.ret == 'notify') {
|
||||||
$.notify('<strong>' + ret.log + '</strong>', {type: 'warning'});
|
$.notify('<strong>' + ret.log + '</strong>', {type: 'warning'});
|
||||||
|
} else if (ret.ret == 'success' || ret == true) {
|
||||||
|
$.notify('명령을 완료했습니다.', {type: 'success'});
|
||||||
}
|
}
|
||||||
on_start();
|
on_start();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
/* ========== Cosmic Violet Theme (Anilife Exclusive) ========== */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #4c1d95 100%) !important;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #e0e7ff;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: 'Inter', 'Noto Sans KR', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styling for Violet Theme */
|
||||||
|
.table {
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
background: rgba(49, 46, 129, 0.4) !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th, .thead-dark th {
|
||||||
|
background: linear-gradient(135deg, rgba(76, 29, 149, 0.9) 0%, rgba(49, 46, 129, 0.9) 100%) !important;
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
border-color: rgba(167, 139, 250, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr {
|
||||||
|
background: rgba(30, 27, 75, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover, .tableRowHover:hover {
|
||||||
|
background: rgba(76, 29, 149, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, .table th {
|
||||||
|
border-color: rgba(167, 139, 250, 0.15) !important;
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Premium Buttons */
|
||||||
|
.custom-btn {
|
||||||
|
padding: 8px 20px; border-radius: 12px; font-weight: 700; font-size: 14px;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex; align-items: center; justify-content: center; color: white !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.btn-reset-queue { background: rgba(59, 130, 246, 0.2); border-color: rgba(59, 130, 246, 0.3); }
|
||||||
|
.btn-reset-queue:hover { background: rgba(59, 130, 246, 0.4); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(59, 130, 246, 0.3); }
|
||||||
|
|
||||||
|
.btn-delete-completed { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.3); }
|
||||||
|
.btn-delete-completed:hover { background: rgba(239, 68, 68, 0.4); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(239, 68, 68, 0.3); }
|
||||||
|
|
||||||
|
.btn-ffmpeg { background: rgba(139, 92, 246, 0.2); border-color: rgba(139, 92, 246, 0.3); }
|
||||||
|
.btn-ffmpeg:hover { background: rgba(139, 92, 246, 0.4); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(139, 92, 246, 0.3); }
|
||||||
|
|
||||||
|
/* Action buttons inside table */
|
||||||
|
.action-btn {
|
||||||
|
padding: 4px 12px; border-radius: 50rem; font-size: 12px; font-weight: 700;
|
||||||
|
transition: all 0.2s; border: 1px solid transparent; background: transparent; color: #94a3b8;
|
||||||
|
}
|
||||||
|
.btn-stop { background: rgba(239, 68, 68, 0.2); color: #f87171; border-color: rgba(239, 68, 68, 0.3); }
|
||||||
|
.btn-stop:hover { background: #ef4444; color: white; transform: scale(1.05); }
|
||||||
|
|
||||||
|
/* Mobile Card Styles */
|
||||||
|
.queue-card {
|
||||||
|
background: rgba(30, 27, 75, 0.4);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.15);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.queue-card:hover { border-color: rgba(167, 139, 250, 0.4); transform: translateY(-3px); }
|
||||||
|
|
||||||
|
.card-header-flex {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: rgba(49, 46, 129, 0.3);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(167, 139, 250, 0.1);
|
||||||
|
}
|
||||||
|
.card-idx { font-weight: 800; color: #818cf8; font-size: 14px; }
|
||||||
|
|
||||||
|
.card-content { padding: 20px; }
|
||||||
|
.card-filename {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f8fafc;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.meta-item i { font-size: 14px; color: #a78bfa; margin-bottom: 2px; }
|
||||||
|
.meta-item span { color: #e2e8f0; font-weight: 600; font-size: 12px; }
|
||||||
|
|
||||||
|
.card-footer-actions { padding: 0 20px 20px 20px; }
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 10px; font-weight: 800; padding: 4px 12px; border-radius: 50rem;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.status-completed { background: rgba(34, 197, 94, 0.2); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.3); }
|
||||||
|
.status-downloading { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
|
||||||
|
.status-wait { background: rgba(148, 163, 184, 0.2); color: #94a3b8; border: 1px solid rgba(148, 163, 184, 0.3); }
|
||||||
|
.status-fail { background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background: rgba(49, 46, 129, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* 로딩 인디케이터 오버라이드 */
|
/* 로딩 인디케이터 오버라이드 */
|
||||||
#loading {
|
#loading {
|
||||||
background: rgba(15, 23, 42, 0.85) !important;
|
background: rgba(30, 27, 75, 0.9) !important;
|
||||||
backdrop-filter: blur(8px) !important;
|
backdrop-filter: blur(8px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,71 +540,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
/* Navigation Menu Override */
|
|
||||||
ul.nav.nav-pills.bg-light {
|
|
||||||
background-color: rgba(30, 41, 59, 0.6) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 50rem !important;
|
|
||||||
padding: 6px !important;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
|
|
||||||
display: inline-flex !important;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
width: auto !important;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-item {
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-link {
|
|
||||||
border-radius: 50rem !important;
|
|
||||||
padding: 8px 20px !important;
|
|
||||||
color: #94a3b8 !important;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-link:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff !important;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-link.active {
|
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Smooth Load Transition */
|
|
||||||
.content-cloak,
|
|
||||||
#menu_module_div,
|
|
||||||
#menu_page_div {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Staggered Delays for Natural Top-Down Flow */
|
|
||||||
#menu_module_div.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 0ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu_page_div.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 150ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-cloak.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation Menu Override (Top Sub-menu) */
|
/* Navigation Menu Override (Top Sub-menu) */
|
||||||
ul.nav.nav-pills.bg-light {
|
ul.nav.nav-pills.bg-light {
|
||||||
background-color: rgba(30, 41, 59, 0.6) !important;
|
background-color: rgba(30, 41, 59, 0.6) !important;
|
||||||
@@ -392,15 +553,17 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.nav.nav-pills .nav-item { margin: 0 2px; }
|
ul.nav.nav-pills .nav-item { margin: 2px; }
|
||||||
ul.nav.nav-pills .nav-link {
|
ul.nav.nav-pills .nav-link {
|
||||||
border-radius: 50rem !important;
|
border-radius: 50rem !important;
|
||||||
padding: 8px 20px !important;
|
padding: 8px 20px !important;
|
||||||
color: #94a3b8 !important;
|
color: #94a3b8 !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
ul.nav.nav-pills .nav-link:hover {
|
ul.nav.nav-pills .nav-link:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
@@ -412,6 +575,24 @@
|
|||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Logic Fix */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
margin-top: 50px !important; /* Ensure visibility below SJVA navbar */
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills .nav-link {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@@ -423,9 +604,15 @@ $(document).ready(function(){
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* Mobile Margin Fix */
|
/* Mobile Margin Fix & Table Scrolling */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
body { overflow-x: hidden !important; padding: 0 !important; margin: 0 !important; }
|
body { overflow-x: hidden !important; }
|
||||||
|
#result_table {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
.container, .container-fluid, .row, form, #program_list, #program_auto_form, #episode_list, .queue-container, #yommi_wrapper, #main_container {
|
.container, .container-fluid, .row, form, #program_list, #program_auto_form, #episode_list, .queue-container, #yommi_wrapper, #main_container {
|
||||||
width: 100% !important; max-width: 100% !important;
|
width: 100% !important; max-width: 100% !important;
|
||||||
padding-left: 4px !important; padding-right: 4px !important;
|
padding-left: 4px !important; padding-right: 4px !important;
|
||||||
|
|||||||
@@ -1,37 +1,67 @@
|
|||||||
{% extends "base.html" %} {% block content %}
|
{% extends "base.html" %} {% block content %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Global Container Margin Overrides */
|
||||||
|
#main_container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding-left: 15px !important;
|
||||||
|
padding-right: 15px !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
.container, .container-fluid {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="content-cloak">
|
<div id="yommi_wrapper" class="container-fluid mt-2 mt-md-4 mx-auto content-cloak" style="max-width: 100%;">
|
||||||
<div id="preloader" class="loader">
|
<div id="preloader" class="loader">
|
||||||
<div class="loader-inner">
|
<div class="loader-inner">
|
||||||
<div class="loader-line-wrap">
|
<div class="loader-line-wrap"><div class="loader-line"></div></div>
|
||||||
<div class="loader-line"></div>
|
<div class="loader-line-wrap"><div class="loader-line"></div></div>
|
||||||
</div>
|
<div class="loader-line-wrap"><div class="loader-line"></div></div>
|
||||||
<div class="loader-line-wrap">
|
<div class="loader-line-wrap"><div class="loader-line"></div></div>
|
||||||
<div class="loader-line"></div>
|
<div class="loader-line-wrap"><div class="loader-line"></div></div>
|
||||||
</div>
|
|
||||||
<div class="loader-line-wrap">
|
|
||||||
<div class="loader-line"></div>
|
|
||||||
</div>
|
|
||||||
<div class="loader-line-wrap">
|
|
||||||
<div class="loader-line"></div>
|
|
||||||
</div>
|
|
||||||
<div class="loader-line-wrap">
|
|
||||||
<div class="loader-line"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="main_content" style="display: none; opacity: 0; transition: opacity 0.3s ease-in;">
|
|
||||||
<form id="program_list">
|
<form id="program_list">
|
||||||
{{ macros.setting_input_text_and_buttons('code', '작품 Code',
|
<div class="card p-2 p-md-4 mb-2 mb-md-4 border-0" style="background: rgba(49, 46, 129, 0.6); backdrop-filter: blur(10px); box-shadow: 0 4px 20px rgba(139, 92, 246, 0.2); border-radius: 16px;">
|
||||||
[['analysis_btn', '분석'], ['go_anilife_btn', 'Go 애니라이프']], desc='예)
|
<div class="form-group mb-0">
|
||||||
"https://anilife.live/g/l?id=f6e83ec6-bd25-4d6c-9428-c10522687604" 이나 "f6e83ec6-bd25-4d6c-9428-c10522687604"')
|
<label for="code" class="text-white font-weight-bold mb-2" style="color: #c4b5fd !important;">
|
||||||
}}
|
<i class="fa fa-search mr-2" style="color: #a78bfa;"></i>작품 Code
|
||||||
|
</label>
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<input type="text" id="code" name="code" class="form-control border-0"
|
||||||
|
placeholder="URL 또는 코드를 입력하세요"
|
||||||
|
style="background: rgba(30, 27, 75, 0.8); color: #e0e7ff; box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); border-radius: 12px 0 0 12px;">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button id="analysis_btn" class="btn px-2 px-md-4 font-weight-bold" type="button"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; box-shadow: 0 0 15px rgba(139, 92, 246, 0.4);">
|
||||||
|
<i class="fa fa-cogs mr-1"></i> 분석
|
||||||
|
</button>
|
||||||
|
<button id="go_anilife_btn" class="btn px-2 px-md-3" type="button"
|
||||||
|
style="background: rgba(167, 139, 250, 0.2); border: 1px solid rgba(167, 139, 250, 0.4); color: #c4b5fd; border-radius: 0 12px 12px 0;">
|
||||||
|
<span class="d-none d-md-inline">Go 애니라이프</span>
|
||||||
|
<span class="d-md-none">Go</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center mt-2 small" style="color: #a78bfa;">
|
||||||
|
<i class="fa fa-info-circle mr-1"></i>
|
||||||
|
<span>예) "https://anilife.live/g/l?id=xxx" 또는 "f6e83ec6-bd25-4d6c-9428-c10522687604"</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form id="program_auto_form">
|
<form id="program_auto_form">
|
||||||
<div id="episode_list"></div>
|
<div id="episode_list"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!--전체-->
|
<!--전체-->
|
||||||
<script src="{{ url_for('.static', filename='js/sjva_ui14.js') }}"></script>
|
<script src="{{ url_for('.static', filename='js/sjva_ui14.js') }}"></script>
|
||||||
@@ -40,7 +70,7 @@
|
|||||||
const package_name = "{{arg['package_name'] }}";
|
const package_name = "{{arg['package_name'] }}";
|
||||||
const sub = "{{arg['sub'] }}";
|
const sub = "{{arg['sub'] }}";
|
||||||
const anilife_url = "{{arg['anilife_url']}}";
|
const anilife_url = "{{arg['anilife_url']}}";
|
||||||
{#let current_data = null;#}
|
// let current_data = null;
|
||||||
|
|
||||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||||
get: (searchParams, prop) => searchParams.get(prop),
|
get: (searchParams, prop) => searchParams.get(prop),
|
||||||
@@ -133,33 +163,21 @@
|
|||||||
episodeList.style.transition = 'opacity 0.3s ease-in';
|
episodeList.style.transition = 'opacity 0.3s ease-in';
|
||||||
|
|
||||||
str = '';
|
str = '';
|
||||||
tmp = '<div class="form-inline">'
|
|
||||||
tmp += m_button('check_download_btn', '선택 다운로드 추가', []);
|
|
||||||
tmp += m_button('all_check_on_btn', '전체 선택', []);
|
|
||||||
tmp += m_button('all_check_off_btn', '전체 해제', []);
|
|
||||||
/*
|
|
||||||
tmp += ' <input id="new_title" name="new_title" class="form-control form-control-sm" value="'+data.title+'">'
|
|
||||||
tmp += '</div>'
|
|
||||||
tmp += m_button('apply_new_title_btn', '저장폴더명, 파일명 제목 변경', []);
|
|
||||||
tmp += m_button('search_tvdb_btn', 'TVDB', []);
|
|
||||||
tmp = m_button_group(tmp)
|
|
||||||
*/
|
|
||||||
str += tmp
|
|
||||||
// program
|
// program
|
||||||
str += m_hr_black();
|
|
||||||
str += m_row_start(0);
|
str += m_row_start(0);
|
||||||
tmp = ''
|
|
||||||
|
let posterHtml = '';
|
||||||
if (data.image != null) {
|
if (data.image != null) {
|
||||||
// CDN 이미지 프록시 적용
|
|
||||||
let proxyImgSrc = data.image;
|
let proxyImgSrc = data.image;
|
||||||
if (data.image && data.image.includes('cdn.anilife.live')) {
|
if (data.image && data.image.includes('cdn.anilife.live')) {
|
||||||
proxyImgSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?image_url=' + encodeURIComponent(data.image);
|
proxyImgSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(data.image);
|
||||||
}
|
}
|
||||||
tmp = '<img src="' + proxyImgSrc + '" class="img-fluid series-main-img" onerror="this.src=\'../static/img_loader_x200.svg\'">';
|
posterHtml = '<div class="series-poster-side mb-3 mb-md-0"><img src="' + proxyImgSrc + '" class="img-fluid series-main-img" onerror="this.src=\'../static/img_loader_x200.svg\'"></div>';
|
||||||
}
|
}
|
||||||
str += m_col(3, tmp)
|
|
||||||
tmp = ''
|
tmp = '';
|
||||||
tmp += m_row_start(2) + m_col(3, '제목', 'right') + m_col(9, '<strong style="font-size:1.3em;">' + data.title + '</strong>') + m_row_end();
|
tmp += '<div class="mb-3"><strong style="font-size:1.6em; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.3);">' + data.title + '</strong></div>';
|
||||||
|
|
||||||
// des1 데이터를 각 항목별로 파싱하여 표시
|
// des1 데이터를 각 항목별로 파싱하여 표시
|
||||||
if (data.des1) {
|
if (data.des1) {
|
||||||
@@ -175,9 +193,22 @@
|
|||||||
// 첫 번째 br 태그 제거 (첫 줄에는 필요없음)
|
// 첫 번째 br 태그 제거 (첫 줄에는 필요없음)
|
||||||
formattedDes = formattedDes.replace(/^<br>/, '');
|
formattedDes = formattedDes.replace(/^<br>/, '');
|
||||||
|
|
||||||
tmp += '<div class="series-info-box">' + formattedDes + '</div>';
|
tmp += '<div class="series-info-box d-flex flex-column flex-md-row animate__animated animate__fadeIn">';
|
||||||
|
if (posterHtml) {
|
||||||
|
tmp += posterHtml;
|
||||||
}
|
}
|
||||||
str += m_col(9, tmp)
|
tmp += '<div class="series-info-side ml-md-4">' + formattedDes + '</div>';
|
||||||
|
tmp += '</div>';
|
||||||
|
|
||||||
|
// Integrated Actions Toolbar (Linkkf Style)
|
||||||
|
tmp += '<div class="d-flex flex-wrap align-items-center gap-2 p-3 mt-3 rounded" style="background: rgba(30, 27, 75, 0.4); border: 1px solid rgba(167, 139, 250, 0.1);">';
|
||||||
|
tmp += '<button id="check_download_btn" class="action-btn action-btn-primary mr-2" style="padding: 8px 16px; font-size: 0.9em;"><i class="fa fa-download"></i> 선택 다운로드</button>';
|
||||||
|
tmp += '<button id="all_check_on_btn" class="action-btn action-btn-secondary mr-2" style="padding: 8px 16px; font-size: 0.9em;"><i class="fa fa-check-square-o"></i> 전체 선택</button>';
|
||||||
|
tmp += '<button id="all_check_off_btn" class="action-btn action-btn-outline mr-3" style="padding: 8px 16px; font-size: 0.9em;"><i class="fa fa-square-o"></i> 해제</button>';
|
||||||
|
tmp += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
str += m_col(12, tmp)
|
||||||
str += m_row_end();
|
str += m_row_end();
|
||||||
|
|
||||||
str += '<div class="episode-list-container">';
|
str += '<div class="episode-list-container">';
|
||||||
@@ -185,7 +216,7 @@
|
|||||||
// CDN 이미지 프록시 적용
|
// CDN 이미지 프록시 적용
|
||||||
let epThumbSrc = data.episode[i].thumbnail || '';
|
let epThumbSrc = data.episode[i].thumbnail || '';
|
||||||
if (epThumbSrc && epThumbSrc.includes('cdn.anilife.live')) {
|
if (epThumbSrc && epThumbSrc.includes('cdn.anilife.live')) {
|
||||||
epThumbSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?image_url=' + encodeURIComponent(epThumbSrc);
|
epThumbSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(epThumbSrc);
|
||||||
}
|
}
|
||||||
|
|
||||||
str += '<div class="episode-card">';
|
str += '<div class="episode-card">';
|
||||||
@@ -291,25 +322,19 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// DOM 로딩 완료 후 콘텐츠 표시
|
// DOM 로딩 완료 후 preloader 숨기기
|
||||||
const mainContent = document.getElementById('main_content');
|
|
||||||
const preloader = document.getElementById('preloader');
|
const preloader = document.getElementById('preloader');
|
||||||
|
|
||||||
// 메인 콘텐츠 보이기 (fade-in 효과)
|
|
||||||
mainContent.style.display = 'block';
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
mainContent.style.opacity = '1';
|
|
||||||
// preloader 숨기기
|
|
||||||
if (preloader) {
|
if (preloader) {
|
||||||
preloader.style.opacity = '0';
|
preloader.style.opacity = '0';
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
preloader.style.display = 'none';
|
preloader.style.display = 'none';
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 500);
|
||||||
|
|
||||||
$("#loader").css("display", 'none');
|
$("#loader").css("display", 'none');
|
||||||
// console.log({{ arg['code'] }})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#analysis_btn").unbind("click").bind('click', function (e) {
|
$("#analysis_btn").unbind("click").bind('click', function (e) {
|
||||||
@@ -452,22 +477,95 @@
|
|||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== Cosmic Violet Theme (Anilife Exclusive) ========== */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #4c1d95 100%) !important;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #e0e7ff;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: 'Inter', 'Noto Sans KR', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-secondary {
|
||||||
|
background: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #c4b5fd;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-secondary:hover {
|
||||||
|
background: rgba(167, 139, 250, 0.35);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: #a78bfa;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-outline:hover {
|
||||||
|
background: rgba(167, 139, 250, 0.15);
|
||||||
|
color: #c4b5fd;
|
||||||
|
}
|
||||||
|
|
||||||
/* 시리즈 정보 박스 스타일 */
|
/* 시리즈 정보 박스 스타일 */
|
||||||
.series-info-box {
|
.series-info-box {
|
||||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%);
|
background: linear-gradient(135deg, rgba(49, 46, 129, 0.95) 0%, rgba(30, 27, 75, 0.95) 100%);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px 25px;
|
padding: 25px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
line-height: 2.2;
|
color: #e0e7ff;
|
||||||
color: #e2e8f0;
|
border: 1px solid rgba(167, 139, 250, 0.2);
|
||||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-poster-side {
|
||||||
|
width: 180px;
|
||||||
|
min-width: 180px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-info-side {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 2.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-info-box strong {
|
.series-info-box strong {
|
||||||
color: #60a5fa;
|
color: #a78bfa;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
min-width: 100px;
|
min-width: 90px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,18 +583,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(15, 23, 42, 0.85) 100%);
|
background: linear-gradient(135deg, rgba(49, 46, 129, 0.85) 0%, rgba(30, 27, 75, 0.85) 100%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.12);
|
border: 1px solid rgba(167, 139, 250, 0.15);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.episode-card:hover {
|
.episode-card:hover {
|
||||||
background: linear-gradient(135deg, rgba(51, 65, 85, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%);
|
background: linear-gradient(135deg, rgba(76, 29, 149, 0.9) 0%, rgba(49, 46, 129, 0.9) 100%);
|
||||||
border-color: rgba(96, 165, 250, 0.5);
|
border-color: rgba(167, 139, 250, 0.5);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 15px rgba(96, 165, 250, 0.2);
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 에피소드 썸네일 */
|
/* 에피소드 썸네일 */
|
||||||
@@ -507,7 +605,7 @@
|
|||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(30, 41, 59, 0.5) 100%);
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(49, 46, 129, 0.5) 100%);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +625,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -548,7 +646,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.episode-title {
|
.episode-title {
|
||||||
color: #e2e8f0;
|
color: #e0e7ff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
@@ -560,7 +658,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.episode-date {
|
.episode-date {
|
||||||
color: #64748b;
|
color: #a78bfa;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -578,14 +676,84 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bootstrap Toggle Custom Styling */
|
||||||
|
.toggle.btn-success, .toggle.btn-success:hover {
|
||||||
|
background: linear-gradient(135deg, #f472b6 0%, #db2777 100%) !important; /* Vibrant Pink/Magenta */
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(236, 72, 153, 0.3) !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.btn-success:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(236, 72, 153, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.btn-secondary {
|
||||||
|
background: rgba(30, 27, 75, 0.6) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-on.btn-success {
|
||||||
|
background: linear-gradient(135deg, #f472b6 0%, #db2777 100%) !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-off.btn-secondary {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-handle {
|
||||||
|
background: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.episode-actions .toggle {
|
.episode-actions .toggle {
|
||||||
transform: scale(0.85);
|
transform: scale(0.9);
|
||||||
|
width: 60px !important;
|
||||||
|
height: 25px !important;
|
||||||
|
min-width: 60px !important;
|
||||||
|
min-height: 25px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 모바일 반응형 - Bootstrap 모든 레이아웃 강제 덮어쓰기 */
|
/* 모바일 반응형 - Bootstrap 모든 레이아웃 강제 덮어쓰기 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
/* 상단 서브메뉴가 SJVA 메인 navbar에 가려지지 않도록 여백 추가 */
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 입력창 크기 최적화 */
|
||||||
|
.input-group.input-group-lg {
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
.input-group.input-group-lg .form-control {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
.input-group.input-group-lg .input-group-append {
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
}
|
||||||
|
.input-group.input-group-lg .btn {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* 전체 페이지 기본 설정 */
|
/* 전체 페이지 기본 설정 */
|
||||||
body {
|
body {
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
|
|||||||
@@ -180,21 +180,20 @@
|
|||||||
str += '<div id="inner_screen_movie" class="row infinite-scroll">';
|
str += '<div id="inner_screen_movie" class="row infinite-scroll">';
|
||||||
for (let i in data.anime_list) {
|
for (let i in data.anime_list) {
|
||||||
|
|
||||||
tmp = '<div class="col-6 col-sm-4 col-md-3">';
|
tmp = '<div class=\"col-6 col-sm-4 col-md-3\">';
|
||||||
tmp += '<div class="card">';
|
tmp += '<div class=\"card\">';
|
||||||
// 이미지 프록시를 통해 CDN 이미지 로드 (hotlink 보호 우회)
|
// 이미지 프록시를 통해 CDN 이미지 로드 (hotlink 보호 우회)
|
||||||
let airingImgUrl = data.anime_list[i].image_link;
|
let airingImgUrl = data.anime_list[i].image_link;
|
||||||
if (airingImgUrl && airingImgUrl.includes('cdn.anilife.live')) {
|
if (airingImgUrl && airingImgUrl.includes('cdn.anilife.live')) {
|
||||||
airingImgUrl = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(airingImgUrl);
|
airingImgUrl = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(airingImgUrl);
|
||||||
}
|
}
|
||||||
tmp += '<img class="lazyload" src="../static/img_loader_x200.svg" data-original="' + airingImgUrl + '" style="cursor: pointer" onerror="this.src=\'../static/img_loader_x200.svg\'" onclick="location.href=\'./request?code=' + data.anime_list[i].code + '\'"/>';
|
tmp += '<div class=\"card-img-container\">';
|
||||||
tmp += '<div class="card-body">'
|
tmp += '<img class=\"lazyload\" src=\"../static/img_loader_x200.svg\" data-original=\"' + airingImgUrl + '\" style=\"cursor: pointer\" onerror=\"this.src=\'../static/img_loader_x200.svg\'\" onclick=\"location.href=\'./request?code=' + data.anime_list[i].code + '\'\"/>';
|
||||||
// {#tmp += '<button id="code_button" data-code="' + data.episode[i].code + '" type="button" class="btn btn-primary code-button bootstrap-tooltip" data-toggle="button" data-tooltip="true" aria-pressed="true" autocomplete="off" data-placement="top">' +#}
|
tmp += '<span class=\"episode-badge\">' + data.anime_list[i].epx + '</span>';
|
||||||
// {# '<span data-tooltip-text="'+data.episode[i].title+'">' + data.episode[i].code + '</span></button></div>';#}
|
tmp += '</div>';
|
||||||
tmp += '<h5 class="card-title">' + data.anime_list[i].title + '</h5>';
|
tmp += '<div class=\"card-body\">'
|
||||||
tmp += '<p class="card-text">' + data.anime_list[i].code + '</p>';
|
tmp += '<h5 class=\"card-title\">' + data.anime_list[i].title + '</h5>';
|
||||||
tmp += '<p class="card-text">' + data.anime_list[i].epx + '</p>';
|
tmp += '<a href=\"./request?code=' + data.anime_list[i].code + '\" class=\"btn btn-primary cut-text\">' + data.anime_list[i].title + '</a>';
|
||||||
tmp += '<a href="./request?code=' + data.anime_list[i].code + '" class="btn btn-primary cut-text">' + data.anime_list[i].title + '</a>';
|
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
@@ -253,13 +252,12 @@
|
|||||||
if (imgUrl && imgUrl.includes('cdn.anilife.live')) {
|
if (imgUrl && imgUrl.includes('cdn.anilife.live')) {
|
||||||
imgUrl = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(imgUrl);
|
imgUrl = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(imgUrl);
|
||||||
}
|
}
|
||||||
|
tmp += '<div class="card-img-container">';
|
||||||
tmp += '<img class="card-img-top" src="' + imgUrl + '" onerror="this.src=\'../static/img_loader_x200.svg\'" />';
|
tmp += '<img class="card-img-top" src="' + imgUrl + '" onerror="this.src=\'../static/img_loader_x200.svg\'" />';
|
||||||
|
tmp += '<span class="episode-badge">' + data.anime_list[i].epx + '</span>';
|
||||||
|
tmp += '</div>';
|
||||||
tmp += '<div class="card-body">'
|
tmp += '<div class="card-body">'
|
||||||
// {#tmp += '<button id="code_button" data-code="' + data.episode[i].code + '" type="button" class="btn btn-primary code-button bootstrap-tooltip" data-toggle="button" data-tooltip="true" aria-pressed="true" autocomplete="off" data-placement="top">' +#}
|
|
||||||
// {# '<span data-tooltip-text="'+data.episode[i].title+'">' + data.episode[i].code + '</span></button></div>';#}
|
|
||||||
tmp += '<h5 class="card-title">' + data.anime_list[i].title + '</h5>';
|
tmp += '<h5 class="card-title">' + data.anime_list[i].title + '</h5>';
|
||||||
tmp += '<p class="card-text">' + data.anime_list[i].code + '</p>';
|
|
||||||
tmp += '<p class="card-text">' + data.anime_list[i].epx + '</p>';
|
|
||||||
tmp += '<a href="' + request_url + '" class="btn btn-primary cut-text">' + data.anime_list[i].title + '</a>';
|
tmp += '<a href="' + request_url + '" class="btn btn-primary cut-text">' + data.anime_list[i].title + '</a>';
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
@@ -304,10 +302,14 @@
|
|||||||
if (screenImgUrl && screenImgUrl.includes('cdn.anilife.live')) {
|
if (screenImgUrl && screenImgUrl.includes('cdn.anilife.live')) {
|
||||||
screenImgUrl = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(screenImgUrl);
|
screenImgUrl = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(screenImgUrl);
|
||||||
}
|
}
|
||||||
|
tmp += '<div class="card-img-container">';
|
||||||
tmp += '<img class="card-img-top" src="' + screenImgUrl + '" onerror="this.src=\'../static/img_loader_x200.svg\'" />';
|
tmp += '<img class="card-img-top" src="' + screenImgUrl + '" onerror="this.src=\'../static/img_loader_x200.svg\'" />';
|
||||||
|
if (data.anime_list[i].epx) {
|
||||||
|
tmp += '<span class="episode-badge">' + data.anime_list[i].epx + '</span>';
|
||||||
|
}
|
||||||
|
tmp += '</div>';
|
||||||
tmp += '<div class="card-body">'
|
tmp += '<div class="card-body">'
|
||||||
tmp += '<h5 class="card-title">' + data.anime_list[i].title + '</h5>';
|
tmp += '<h5 class="card-title">' + data.anime_list[i].title + '</h5>';
|
||||||
tmp += '<p class="card-text">' + data.anime_list[i].code + '</p>';
|
|
||||||
tmp += '<a href="./request?code=' + data.anime_list[i].code + '" class="btn btn-primary cut-text">' + data.anime_list[i].title + '</a>';
|
tmp += '<a href="./request?code=' + data.anime_list[i].code + '" class="btn btn-primary cut-text">' + data.anime_list[i].title + '</a>';
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
tmp += '</div>';
|
tmp += '</div>';
|
||||||
@@ -762,6 +764,27 @@
|
|||||||
border-radius: 12px 12px 0 0;
|
border-radius: 12px 12px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Card Image Container for Badge Overlay */
|
||||||
|
.card-img-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Episode Badge Overlay */
|
||||||
|
.episode-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
padding: 12px !important;
|
padding: 12px !important;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -854,11 +877,162 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: NanumSquareNeo, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Noto Sans, Liberation Sans, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
font-family: 'Inter', 'Noto Sans KR', NanumSquareNeo, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== Cosmic Violet Theme (Anilife Exclusive) ========== */
|
||||||
body {
|
body {
|
||||||
background-image: linear-gradient(90deg, #233f48, #6c6fa2, #768dae);
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #4c1d95 100%) !important;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #e0e7ff;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Bar Styling */
|
||||||
|
#yommi_wrapper {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
background: rgba(49, 46, 129, 0.5);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group #input_search {
|
||||||
|
background: rgba(30, 27, 75, 0.7) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.2) !important;
|
||||||
|
color: #e0e7ff !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group #input_search::placeholder {
|
||||||
|
color: #c4b5fd;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group #input_search:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #a78bfa !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group #btn_search {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 10px 24px !important;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group #btn_search:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Buttons */
|
||||||
|
#anime_category {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn {
|
||||||
|
background: rgba(49, 46, 129, 0.5) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.3) !important;
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
padding: 8px 20px !important;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.3) !important;
|
||||||
|
border-color: #a78bfa !important;
|
||||||
|
color: #fff !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-success {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-dark {
|
||||||
|
background: linear-gradient(135deg, #475569 0%, #334155 100%) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anime_category .btn-grey {
|
||||||
|
background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Badge */
|
||||||
|
.btn-info {
|
||||||
|
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-warning {
|
||||||
|
background: #fbbf24 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Styling Override */
|
||||||
|
.card {
|
||||||
|
background: rgba(49, 46, 129, 0.5) !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.15) !important;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: rgba(167, 139, 250, 0.4) !important;
|
||||||
|
box-shadow: 0 8px 30px rgba(139, 92, 246, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
background: linear-gradient(180deg, rgba(30, 27, 75, 0.85) 0%, rgba(30, 27, 75, 0.95) 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: #a78bfa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
color: #c4b5fd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .btn-primary:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
#spinner {
|
||||||
|
color: #a78bfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo {
|
.demo {
|
||||||
@@ -1055,7 +1229,18 @@ $(document).ready(function(){
|
|||||||
<style>
|
<style>
|
||||||
/* Mobile Margin Fix */
|
/* Mobile Margin Fix */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
body { overflow-x: hidden !important; padding: 0 !important; margin: 0 !important; }
|
body { overflow-x: hidden !important; padding: 0 !important; margin: 0 !important; padding-top: 10px !important; }
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 50px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills .nav-link {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.container, .container-fluid, .row, form, #program_list, #program_auto_form, #episode_list, .queue-container, #yommi_wrapper, #main_container {
|
.container, .container-fluid, .row, form, #program_list, #program_auto_form, #episode_list, .queue-container, #yommi_wrapper, #main_container {
|
||||||
width: 100% !important; max-width: 100% !important;
|
width: 100% !important; max-width: 100% !important;
|
||||||
padding-left: 4px !important; padding-right: 4px !important;
|
padding-left: 4px !important; padding-right: 4px !important;
|
||||||
@@ -1076,6 +1261,12 @@ $(document).ready(function(){
|
|||||||
white-space: normal !important; text-align: left !important;
|
white-space: normal !important; text-align: left !important;
|
||||||
line-height: 1.4 !important; height: auto !important; display: inline-block !important;
|
line-height: 1.4 !important; height: auto !important; display: inline-block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Search Button Size Reduction */
|
||||||
|
.input-group #btn_search {
|
||||||
|
padding: 8px 14px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -45,7 +45,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ macros.setting_input_int('anilife_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['anilife_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
|
{{ macros.setting_input_int('anilife_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['anilife_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
|
||||||
{{ macros.setting_select('anilife_download_method', '다운로드 방법', [['ffmpeg', 'ffmpeg (기본)'], ['ytdlp', 'yt-dlp']], value=arg.get('anilife_download_method', 'ffmpeg'), desc='m3u8 다운로드에 사용할 도구를 선택합니다.') }}
|
{{ macros.setting_select('anilife_download_method', '다운로드 방법', [['ffmpeg', 'ffmpeg (기본)'], ['ytdlp', 'yt-dlp (단일쓰레드)'], ['aria2c', 'yt-dlp (멀티쓰레드/aria2c)']], value=arg.get('anilife_download_method', 'ffmpeg'), desc='m3u8 다운로드에 사용할 도구를 선택합니다.') }}
|
||||||
|
<div id="anilife_download_threads_div">
|
||||||
|
{{ macros.setting_select('anilife_download_threads', '다운로드 속도', [['1', '1배속 (1개, 안정)'], ['2', '2배속 (2개, 권장)'], ['4', '4배속 (4개)'], ['8', '8배속 (8개)'], ['16', '16배속 (16개, 빠름)']], value=arg.get('anilife_download_threads', '16'), desc='yt-dlp 모드에서 사용할 동시 다운로드 수입니다.') }}
|
||||||
|
</div>
|
||||||
{{ macros.setting_checkbox('anilife_order_desc', '요청 화면 최신순 정렬', value=arg['anilife_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
|
{{ macros.setting_checkbox('anilife_order_desc', '요청 화면 최신순 정렬', value=arg['anilife_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
|
||||||
{{ macros.setting_checkbox('anilife_auto_make_folder', '제목 폴더 생성', value=arg['anilife_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
|
{{ macros.setting_checkbox('anilife_auto_make_folder', '제목 폴더 생성', value=arg['anilife_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
|
||||||
<div id="anilife_auto_make_folder_div" class="collapse pl-4 border-left ml-3" style="border-color: rgba(255,255,255,0.1) !important;">
|
<div id="anilife_auto_make_folder_div" class="collapse pl-4 border-left ml-3" style="border-color: rgba(255,255,255,0.1) !important;">
|
||||||
@@ -304,6 +307,22 @@
|
|||||||
.folder-item.selected {
|
.folder-item.selected {
|
||||||
background: rgba(59, 130, 246, 0.3) !important;
|
background: rgba(59, 130, 246, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding-top: 10px !important; }
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 50px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
ul.nav.nav-pills .nav-link {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@@ -323,6 +342,22 @@ $('#ani365_auto_make_folder').change(function() {
|
|||||||
use_collapse('anilife_auto_make_folder');
|
use_collapse('anilife_auto_make_folder');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggle_download_threads() {
|
||||||
|
var method = $('#anilife_download_method').val();
|
||||||
|
if (method == 'ytdlp' || method == 'aria2c') {
|
||||||
|
$('#anilife_download_threads_div').slideDown();
|
||||||
|
} else {
|
||||||
|
$('#anilife_download_threads_div').slideUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#anilife_download_method').change(function() {
|
||||||
|
toggle_download_threads();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
toggle_download_threads();
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#go_btn', function(e){
|
$("body").on('click', '#go_btn', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -546,6 +546,143 @@ $(document).ready(function(){
|
|||||||
}
|
}
|
||||||
.playlist-item:hover { background: rgba(16, 185, 129, 0.1); color: #fff; }
|
.playlist-item:hover { background: rgba(16, 185, 129, 0.1); color: #fff; }
|
||||||
.playlist-item.active { background: rgba(16, 185, 129, 0.2); color: #10b981; font-weight: 600; border-left: 3px solid #10b981; }
|
.playlist-item.active { background: rgba(16, 185, 129, 0.2); color: #10b981; font-weight: 600; border-left: 3px solid #10b981; }
|
||||||
|
|
||||||
|
/* ========== Mobile Video Modal Fix ========== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 모달 크기 조정 */
|
||||||
|
.modal-dialog.modal-xl {
|
||||||
|
margin: 10px auto !important;
|
||||||
|
max-width: calc(100vw - 20px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 비디오 높이 제한 */
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 45vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 높이 제한 */
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 25vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 컨트롤 간소화 */
|
||||||
|
.playlist-controls {
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
}
|
||||||
|
.playlist-controls > div {
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
#current-video-title {
|
||||||
|
font-size: 12px !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
#playlist-progress {
|
||||||
|
font-size: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 네비게이션 z-index 보장 */
|
||||||
|
ul.nav.nav-pills {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 콘텐츠 상단 여백 */
|
||||||
|
.content-cloak {
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 아이템 액션 버튼 스크롤 */
|
||||||
|
.item-actions {
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 전체 레이아웃 */
|
||||||
|
body { overflow-x: hidden !important; }
|
||||||
|
.container, .container-fluid, #main_container {
|
||||||
|
width: 100% !important; max-width: 100% !important;
|
||||||
|
padding-left: 4px !important; padding-right: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 초소형 모바일 (400px 미만) */
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 35vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 20vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-nav-btn {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-toggle-btn {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
.item-meta {
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Nav Pills Fix */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 50px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Notify Styling (Forest Theme) */
|
||||||
|
.bootstrap-notify-container,
|
||||||
|
[data-notify="container"] {
|
||||||
|
max-width: 90vw !important;
|
||||||
|
width: auto !important;
|
||||||
|
right: 5vw !important;
|
||||||
|
left: 5vw !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
background: rgba(2, 44, 34, 0.95) !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3) !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4) !important;
|
||||||
|
color: #ecfdf5 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-success {
|
||||||
|
border-color: rgba(52, 211, 153, 0.4) !important;
|
||||||
|
background: rgba(6, 78, 59, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-warning {
|
||||||
|
border-color: rgba(251, 191, 36, 0.4) !important;
|
||||||
|
background: rgba(120, 53, 15, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-danger {
|
||||||
|
border-color: rgba(239, 68, 68, 0.4) !important;
|
||||||
|
background: rgba(127, 29, 29, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="message"] {
|
||||||
|
color: #ecfdf5 !important;
|
||||||
|
}
|
||||||
|
[data-notify="title"] {
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|||||||
@@ -1367,6 +1367,58 @@ $(document).ready(function(){
|
|||||||
}
|
}
|
||||||
.playlist-item:hover { background: rgba(16, 185, 129, 0.1); color: #fff; }
|
.playlist-item:hover { background: rgba(16, 185, 129, 0.1); color: #fff; }
|
||||||
.playlist-item.active { background: rgba(16, 185, 129, 0.2); color: #10b981; font-weight: 600; border-left: 3px solid #10b981; }
|
.playlist-item.active { background: rgba(16, 185, 129, 0.2); color: #10b981; font-weight: 600; border-left: 3px solid #10b981; }
|
||||||
|
|
||||||
|
/* ========== Mobile Video Modal Fix ========== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 모달 크기 조정 */
|
||||||
|
#videoModal .modal-dialog {
|
||||||
|
margin: 10px auto !important;
|
||||||
|
max-width: calc(100vw - 20px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모달 바디 높이 조정 */
|
||||||
|
#videoModal .modal-body {
|
||||||
|
height: auto !important;
|
||||||
|
max-height: 75vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 비디오 높이 제한 */
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 40vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 높이 제한 */
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 20vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 컨트롤 간소화 */
|
||||||
|
.playlist-nav-btn {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-toggle-btn {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-progress {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 초소형 모바일 (400px 미만) */
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 35vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 15vh !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
background-image: radial-gradient(circle at top right, #1e293b 0%, transparent 60%), radial-gradient(circle at bottom left, #1e293b 0%, transparent 60%);
|
background-image: radial-gradient(circle at top right, #1e293b 0%, transparent 60%), radial-gradient(circle at bottom left, #1e293b 0%, transparent 60%);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
overflow: hidden; /* 외부 스크롤 방지 - 흔들림 해결 */
|
/* overflow: hidden 제거 - 모바일 스크롤 허용 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container & Typography */
|
/* Container & Typography */
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: auto !important; /* 세로 스크롤 허용 */
|
||||||
|
}
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
padding: 4px; /* 모바일 더 작은 여백 */
|
padding: 4px; /* 모바일 더 작은 여백 */
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,12 @@
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
/* 로그 테이블 뷰포트 기반 높이 */
|
||||||
|
textarea#log, textarea#add {
|
||||||
|
max-height: 60vh !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 300px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
|||||||
@@ -1071,5 +1071,118 @@ $(document).ready(function(){
|
|||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== Mobile Video Modal Fix ========== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 모달 크기 조정 - 비디오 모달만 */
|
||||||
|
#videoModal .modal-dialog {
|
||||||
|
margin: 10px auto !important;
|
||||||
|
max-width: calc(100vw - 20px) !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 비디오 높이 제한 */
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 45vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 높이 제한 */
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 25vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 컨트롤 간소화 */
|
||||||
|
.playlist-controls {
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
}
|
||||||
|
.playlist-controls > div {
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
#current-video-title {
|
||||||
|
font-size: 12px !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
#playlist-progress {
|
||||||
|
font-size: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 아이템 액션 버튼 스크롤 */
|
||||||
|
.item-actions {
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 초소형 모바일 (400px 미만) */
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 35vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 20vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-nav-btn {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-toggle-btn {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Nav Pills Fix */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
ul.nav.nav-pills.bg-light {
|
||||||
|
margin-top: 50px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Notify Styling (Slate Blue Theme) */
|
||||||
|
.bootstrap-notify-container,
|
||||||
|
[data-notify="container"] {
|
||||||
|
max-width: 90vw !important;
|
||||||
|
width: auto !important;
|
||||||
|
right: 5vw !important;
|
||||||
|
left: 5vw !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
background: rgba(15, 23, 42, 0.95) !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3) !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4) !important;
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-success {
|
||||||
|
border-color: rgba(34, 197, 94, 0.4) !important;
|
||||||
|
background: rgba(21, 128, 61, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-warning {
|
||||||
|
border-color: rgba(251, 191, 36, 0.4) !important;
|
||||||
|
background: rgba(120, 53, 15, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="container"].alert-danger {
|
||||||
|
border-color: rgba(239, 68, 68, 0.4) !important;
|
||||||
|
background: rgba(127, 29, 29, 0.95) !important;
|
||||||
|
}
|
||||||
|
[data-notify="message"] {
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
[data-notify="title"] {
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1376,6 +1376,58 @@ $(document).ready(function(){
|
|||||||
}
|
}
|
||||||
.playlist-item:hover { background: rgba(16, 185, 129, 0.1); color: #fff; }
|
.playlist-item:hover { background: rgba(16, 185, 129, 0.1); color: #fff; }
|
||||||
.playlist-item.active { background: rgba(16, 185, 129, 0.2); color: #10b981; font-weight: 600; border-left: 3px solid #10b981; }
|
.playlist-item.active { background: rgba(16, 185, 129, 0.2); color: #10b981; font-weight: 600; border-left: 3px solid #10b981; }
|
||||||
|
|
||||||
|
/* ========== Mobile Video Modal Fix ========== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 모달 크기 조정 */
|
||||||
|
#videoModal .modal-dialog {
|
||||||
|
margin: 10px auto !important;
|
||||||
|
max-width: calc(100vw - 20px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모달 바디 높이 조정 */
|
||||||
|
#videoModal .modal-body {
|
||||||
|
height: auto !important;
|
||||||
|
max-height: 75vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 비디오 높이 제한 */
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 40vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 높이 제한 */
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 20vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 플레이리스트 컨트롤 간소화 */
|
||||||
|
.playlist-nav-btn {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-toggle-btn {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-progress {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 초소형 모바일 (400px 미만) */
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
#video-player, .video-js {
|
||||||
|
max-height: 35vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlist-list-container {
|
||||||
|
max-height: 15vh !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user