v0.5.1: Mobile UX improvements - Custom notify styling, nav margin fixes, search button optimization

This commit is contained in:
2026-01-02 15:37:55 +09:00
parent 4e9203ed00
commit c662d2dadc
16 changed files with 2215 additions and 592 deletions

View File

@@ -1,5 +1,5 @@
title: "애니 다운로더"
version: "0.4.1"
version: "0.4.2"
package_name: "anime_downloader"
developer: "projectdx"
description: "anime downloader"

View File

@@ -110,7 +110,7 @@ class Util(object):
i = 0
while i < len(lines):
line = lines[i].strip()
# WEBVTT, NOTE, STYLE 등 메타데이터 스킵
# WEBWTT, NOTE, STYLE 등 메타데이터 스킵
if line.startswith("WEBVTT") or line.startswith("NOTE") or line.startswith("STYLE"):
i += 1
continue
@@ -135,3 +135,50 @@ class Util(object):
# 캡션 텍스트가 바로 나오는 경우 등을 대비
i += 1
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())

View File

@@ -57,7 +57,7 @@ from .lib.crawler import Crawler
# 패키지
# 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
T = TypeVar("T")
@@ -78,6 +78,7 @@ class LogicAniLife(AnimeModuleBase):
"anilife_finished_insert": "[완결]",
"anilife_max_ffmpeg_process_count": "1",
"anilife_download_method": "ffmpeg", # ffmpeg or ytdlp
"anilife_download_threads": "16", # yt-dlp/aria2c 병렬 쓰레드 수
"anilife_order_desc": "False",
"anilife_auto_start": "False",
"anilife_interval": "* 5 * * *",
@@ -164,9 +165,42 @@ class LogicAniLife(AnimeModuleBase):
def __init__(self, P):
super(LogicAniLife, self).__init__(P, setup_default=self.db_default, name=name, first_menu='setting', scheduler_desc="애니라이프 자동 다운로드")
self.queue = None
self.web_list_model = ModelAniLifeItem
self.OS_PLATFORM = platform.system()
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
def get_html(
self,
@@ -578,11 +612,18 @@ class LogicAniLife(AnimeModuleBase):
socketio.emit(
"notify", notify, namespace="/framework", broadcast=True
)
thread = threading.Thread(target=func, args=())
thread.daemon = True
thread.start()
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":
return jsonify(ModelAniLifeItem.web_list(request))
elif sub == "db_remove":
@@ -657,48 +698,14 @@ class LogicAniLife(AnimeModuleBase):
except Exception as e:
logger.error(f"browse_dir error: {e}")
return jsonify({"ret": "error", "error": str(e)}), 500
return jsonify({"ret": "fail", "log": f"Unknown sub: {sub}"})
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())
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
def add_whitelist(*args):
@@ -765,16 +772,50 @@ class LogicAniLife(AnimeModuleBase):
self.queue = FfmpegQueue(
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.queue.queue_start()
# Camoufox 미리 준비 (백그라운드에서 설치 및 바이너리 다운로드)
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):
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'
if "all" in content_code_list:
ret_data = LogicAniLife.get_auto_anime_info(self, url=url)
@@ -1158,9 +1199,39 @@ class LogicAniLife(AnimeModuleBase):
return data
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())
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):
@@ -1210,8 +1281,27 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
self.content_title = None
self.srt_url = None
self.headers = None
# [Lazy Extraction] __init__에서는 무거운 분석을 하지 않습니다.
# self.make_episode_info()
self.filename = info.get("title")
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):
self.module_logic.socketio_callback("status", self.as_dict())
@@ -1223,17 +1313,30 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
tmp["vtt"] = self.vtt
tmp["season"] = self.season
tmp["content_title"] = self.content_title
# 큐 리스트에서 '에피소드 제목'으로 명확히 인지되도록 함
tmp["episode_title"] = self.info.get("title")
tmp["anilife_info"] = self.info
tmp["epi_queue"] = self.epi_queue
tmp["filename"] = self.filename
return tmp
def donwload_completed(self):
db_entity = ModelAniLifeItem.get_by_anilife_id(self.info["_id"])
if db_entity is not None:
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()
# Discord 알림 (이미 메인에서 처리될 수도 있으나 명시적으로 필요한 경우)
# if self.P.ModelSetting.get_bool('anilife_discord_notification'):
# ...
def prepare_extra(self):
"""
[Lazy Extraction] prepare_extra() replaces make_episode_info()
@@ -1305,7 +1408,11 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
def log_stderr(pipe):
for line in iter(pipe.readline, ''):
if line.strip():
logger.info(f"[Camoufox] {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()}")
stderr_thread = threading.Thread(target=log_stderr, args=(process.stderr,))
stderr_thread.start()
@@ -1314,7 +1421,13 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
for line in iter(process.stdout.readline, ''):
stdout_data.append(line)
process.wait(timeout=120)
try:
process.wait(timeout=120)
except subprocess.TimeoutExpired:
logger.error("Camoufox subprocess timed out (120s)")
process.kill()
return
stderr_thread.join(timeout=5)
stdout_full = "".join(stdout_data)
@@ -1468,26 +1581,25 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
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}")
# anilife 전용 다운로드 경로 설정 (ohli24_download_path 대신 anilife_download_path 사용)
# anilife 전용 다운로드 경로 설정
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}")
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:
folder_name = "%s %s" % (
P.ModelSetting.get("ohli24_finished_insert"),
P.ModelSetting.get("anilife_finished_insert"),
self.content_title,
)
else:
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)
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, "Season %s" % int(self.season)
)
@@ -1547,12 +1659,17 @@ class ModelAniLifeItem(db.Model):
def as_dict(self):
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"] = (
self.completed_time.strftime("%Y-%m-%d %H:%M:%S")
if self.completed_time is not 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
def save(self):
@@ -1569,9 +1686,33 @@ class ModelAniLifeItem(db.Model):
@classmethod
def delete_by_id(cls, idx):
db.session.query(cls).filter_by(id=idx).delete()
db.session.commit()
return True
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()
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
def web_list(cls, req):
@@ -1622,22 +1763,28 @@ class ModelAniLifeItem(db.Model):
@classmethod
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.content_code = q["content_code"]
item.season = q["season"]
item.episode_no = q["epi_queue"]
item.episode_no = q.get("epi_queue")
item.title = q["content_title"]
item.episode_title = q["title"]
item.ohli24_va = q["va"]
item.ohli24_vi = q["_vi"]
item.ohli24_id = q["_id"]
item.anilife_va = q.get("va")
item.anilife_vi = q.get("_vi")
item.anilife_id = q["_id"]
item.quality = q["quality"]
item.filepath = q["filepath"]
item.filename = q["filename"]
item.savepath = q["savepath"]
item.video_url = q["url"]
item.vtt_url = q["vtt"]
item.thumbnail = q["thumbnail"]
item.filepath = q.get("filepath")
item.filename = q.get("filename")
item.savepath = q.get("savepath")
item.video_url = q.get("url")
item.vtt_url = q.get("vtt")
item.thumbnail = q.get("thumbnail")
item.status = "wait"
item.ohli24_info = q["anilife_info"]
item.anilife_info = q.get("anilife_info")
item.save()

View File

@@ -131,6 +131,8 @@ class AnimeModuleBase(PluginModuleBase):
arg3 = request.form.get('arg3') or request.args.get('arg3')
return self.process_command(command, arg1, arg2, arg3, req)
return jsonify({'ret': 'fail', 'log': f"Unknown sub: {sub}"})
except Exception as e:
self.P.logger.error(f"AJAX Error: {e}")
self.P.logger.error(traceback.format_exc())

View File

@@ -1691,9 +1691,10 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
"""
from .lib.downloader_factory import DownloaderFactory
# 설정에서 다운로드 방식 읽기
# 설정에서 다운로드 방식 및 쓰레드 수 읽기
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(
method=method,
@@ -1702,6 +1703,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
headers=self.headers,
callback=callback,
callback_id="linkkf",
threads=threads,
callback_function=callback_function
)

View File

@@ -681,12 +681,119 @@
background-color: #e0ff42;
}
/* ========== Cosmic Violet Theme (Anilife Exclusive) ========== */
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 {
background-image: linear-gradient(90deg, #233f48, #6c6fa2, #768dae);
/* Search Bar Styling */
.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 {
@@ -809,5 +916,21 @@
z-index: 99999;
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>
{% endblock %}

View File

@@ -1,305 +1,662 @@
{% extends "base.html" %}
{% 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">
<form id="form_search" class="form-inline" style="text-align:left">
<div class="container-fluid">
<div class="row show-grid">
<span class="col-md-4">
<select id="order" name="order" class="form-control form-control-sm">
<option value="desc">최근</option>
<option value="asc">오래된순</option>
</select>
<select id="option" name="option" class="form-control form-control-sm">
<option value="all">전체</option>
<option value="completed">완료</option>
</select>
</span>
<span class="col-md-8">
<input id="search_word" name="search_word" class="form-control form-control-sm w-75" type="text" placeholder="" aria-label="Search">
<button id="search" class="btn btn-sm btn-outline-success">검색</button>
<button id="reset_btn" class="btn btn-sm btn-outline-success">리셋</button>
</span>
</div>
<form id="form_search" class="form-inline" style="text-align:left; width:100%;">
<div class="search-container">
<div class="search-group-left">
<select id="order" name="order" class="form-control custom-select">
<option value="desc">최근순</option>
<option value="asc">오래된 </option>
</select>
<select id="option" name="option" class="form-control custom-select">
<option value="all">전체</option>
<option value="completed">완료</option>
</select>
</div>
<div class="search-group-right">
<input id="search_word" name="search_word" class="form-control custom-input" type="text" placeholder="애니라이프 검색..." aria-label="Search">
<button id="search" class="btn custom-btn btn-search"><i class="fa fa-search"></i> 검색</button>
<button id="delete_selected_btn" class="btn custom-btn btn-delete-selected" style="display:none;"><i class="fa fa-trash"></i> 선택 삭제</button>
<button id="reset_btn" class="btn custom-btn btn-reset"><i class="fa fa-refresh"></i> 초기화</button>
</div>
</div>
</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>
</form>
<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='page2'></div>
<div id='page1'></div>
<div id="list_div"></div>
<div id='page2'></div>
</div>
<script type="text/javascript">
var package_name = "{{arg['package_name']}}";
var sub = "{{arg['sub']}}";
var current_data = null;
<!-- 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>&times;</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>
$(document).ready(function(){
global_sub_request_search('1');
});
<!-- 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>&times;</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>
$("#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();
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({
url: '/'+package_name+'/ajax/'+sub+ '/db_remove',
type: "POST",
cache: false,
data: {id:id},
dataType: "json",
success: function (data) {
if (data) {
$.notify('<strong>삭제되었습니다.</strong>', {
type: 'success'
});
global_sub_request_search(current_data.paging.current_page, false)
} else {
$.notify('<strong>삭제 실패</strong>', {
type: 'warning'
});
}
}
});
});
$("body").on('click', '#request_btn', function(e){
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 = '';
for (i in data) {
//console.log(data[i])
str += m_row_start();
str += m_col(1, data[i].id);
tmp = (data[i].status == 'completed') ? '완료' : '미완료';
str += m_col(1, tmp);
tmp = data[i].created_time + '(추가)';
if (data[i].completed_time != null)
tmp += data[i].completed_time + '(완료)';
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;
}
</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 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">
$(document).ready(function(){
// Smooth Load Trigger
setTimeout(function() {
$('.content-cloak, #menu_module_div, #menu_page_div').addClass('visible');
}, 100);
});
var package_name = "{{arg['package_name']}}";
var sub = "{{arg['sub']}}";
var current_data = null;
$(document).ready(function(){
setTimeout(function() { $('.content-cloak, #menu_page_div').addClass('visible'); }, 100);
global_sub_request_search('1');
});
function global_sub_request_search(page, move_top = true) {
var formData = get_formdata('#form_search')
formData += '&page=' + page;
$.ajax({
url: '/' + package_name + '/ajax/' + sub + '/web_list',
type: "POST",
cache: false,
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",
success: function (data) {
if (data) {
$.notify('DB를 초기화하였습니다.', {type: 'success'});
global_sub_request_search('1');
} else {
$.notify('초기화 실패', {type: 'warning'});
}
}
});
});
function make_list(data) {
str = '';
for (i in data) {
str += '<div class="item-row animate__animated animate__fadeInUp" style="animation-delay: ' + (i * 0.05) + 's;">';
// Poster
str += '<div class="poster-container">';
if (data[i].image_link) {
// Use proxy to bypass Referer restriction
var proxy_url = '/' + package_name + '/ajax/' + sub + '/proxy_image?url=' + encodeURIComponent(data[i].image_link);
str += '<img src="' + proxy_url + '" class="poster-img" loading="lazy" onerror="this.src=\'https://placehold.co/100x140?text=No+Image\'">';
}
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>
<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 %}

View File

@@ -2,26 +2,39 @@
{% block content %}
<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>
<table id="result_table" class="table table-sm tableRowHover">
<thead class="thead-dark">
<tr>
<th style="width:5%; text-align:center;">IDX</th>
<th style="width:8%; text-align:center;">Plugin</th>
<th style="width:10%; text-align:center;">시작시간</th>
<th style="width:20%; text-align:center;">파일명</th>
<th style="width:8%; text-align:center;">상태</th>
<th style="width:15%; text-align:center;">진행률</th>
<th style="width:5%; text-align:center;">길이</th>
<th style="width:5%; text-align:center;">PF</th>
<th style="width:8%; text-align:center;">배속</th>
<th style="width:8%; text-align:center;">진행시간</th>
<th style="width:8%; text-align:center;">Action</th>
</tr>
</thead>
<tbody id="list"></tbody>
</table>
<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">
<thead class="thead-dark">
<tr>
<th style="width:5%; text-align:center;">IDX</th>
<th style="width:8%; text-align:center;">Plugin</th>
<th style="width:10%; text-align:center;">시작시간</th>
<th style="width:20%; text-align:center;">파일명</th>
<th style="width:8%; text-align:center;">상태</th>
<th style="width:15%; text-align:center;">진행률</th>
<th style="width:5%; text-align:center;">길이</th>
<th style="width:5%; text-align:center;">PF</th>
<th style="width:8%; text-align:center;">배속</th>
<th style="width:8%; text-align:center;">진행시간</th>
<th style="width:8%; text-align:center;">Action</th>
</tr>
</thead>
<tbody id="list"></tbody>
</table>
</div>
<!-- Mobile Card View -->
<div id="list_mobile" class="d-md-none"></div>
<div id='page2'></div>
<script type="text/javascript">
const package_name = "{{arg['package_name'] }}";
@@ -36,21 +49,30 @@
dataType: "json",
global: !silent,
success: function (data) {
// entity_list 응답을 처리
current_data = data;
// 목록 개수가 변했거나 데이터가 없을 때만 전체 갱신 (반짝임 방지)
const list_body = $("#list");
const list_mobile = $("#list_mobile");
if (data.length == 0) {
list_body.html("<tr><td colspan='11'><h4>작업이 없습니다.</h4><td><tr>");
} else if (list_body.children().length !== data.length * 2) { // make_item이 행 2개를 생성하므로
str = ''
for (i in data) {
str += make_item(data[i]);
}
list_body.html(str);
list_mobile.html("<div class='text-center p-5'><h4>작업이 없습니다.</h4></div>");
} 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) {
status_html(data[i]);
}
@@ -99,12 +121,13 @@
});
socket.on('add', function (data) {
str = make_item(data);
if (current_data == null || current_data.length == 0) {
current_data = Array();
$("#list").html(str);
$("#list").html(make_item(data));
$("#list_mobile").html(make_item_mobile(data));
} else {
$("#list").html($("#list").html() + str);
$("#list").append(make_item(data));
$("#list_mobile").append(make_item_mobile(data));
}
current_data.push(data);
});
@@ -121,16 +144,20 @@
globalSendCommand('list', null, null, null, function (data) {
current_data = data;
$("#list").html('');
// console.log(data)
$("#list_mobile").html('');
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 {
str = ''
var str_table = '';
var str_mobile = '';
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) {
e.stopPropagation();
e.preventDefault();
globalSendCommand('stop', $(this).data('idx'), null, null, function (ret) {
refresh_item(ret.data);
var idx = $(this).data('idx');
globalSendCommand('stop', idx, null, null, function (ret) {
// refresh_item is legacy, on_start will handle it or socket will
});
});
function refresh_item(data) {
$('#tr1_' + data.idx).html(make_item1(data));
$('#collapse_' + data.idx).html(make_item2(data));
function make_item_mobile(data) {
var str = '';
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) {
@@ -214,25 +264,51 @@
}
function button_html(data) {
//console.log(data)
str = '';
var btn_str = '';
var btn_mobile_str = '';
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) {
// Table Update
var progress = document.getElementById("progress_" + data.idx);
if (!progress) return;
progress.style.width = data.percent + '%';
progress.innerHTML = data.percent + '%';
progress.style.visibility = 'visible';
document.getElementById("status_" + data.idx).innerHTML = data.status_kor;
document.getElementById("current_pf_count_" + data.idx).innerHTML = data.current_pf_count;
document.getElementById("current_speed_" + data.idx).innerHTML = data.current_speed;
document.getElementById("download_time_" + data.idx).innerHTML = data.download_time;
document.getElementById("detail_" + data.idx).innerHTML = get_detail(data);
if (progress) {
progress.style.width = data.percent + '%';
progress.innerHTML = data.percent + '%';
progress.style.visibility = 'visible';
document.getElementById("status_" + data.idx).innerHTML = data.status_kor;
document.getElementById("current_pf_count_" + data.idx).innerHTML = data.current_pf_count;
document.getElementById("current_speed_" + data.idx).innerHTML = data.current_speed;
document.getElementById("download_time_" + data.idx).innerHTML = data.download_time;
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) {
@@ -242,6 +318,17 @@
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) {
$.ajax({
url: '/' + package_name + '/ajax/' + sub + '/queue_command',
@@ -252,17 +339,156 @@
success: function (ret) {
if (ret.ret == 'notify') {
$.notify('<strong>' + ret.log + '</strong>', {type: 'warning'});
} else if (ret.ret == 'success' || ret == true) {
$.notify('명령을 완료했습니다.', {type: 'success'});
}
on_start();
}
});
}
</script>
</script>
<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 {
background: rgba(15, 23, 42, 0.85) !important;
background: rgba(30, 27, 75, 0.9) !important;
backdrop-filter: blur(8px) !important;
}
@@ -314,71 +540,6 @@
border-radius: 50%;
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) */
ul.nav.nav-pills.bg-light {
background-color: rgba(30, 41, 59, 0.6) !important;
@@ -392,15 +553,17 @@
justify-content: center;
width: auto !important;
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 {
border-radius: 50rem !important;
padding: 8px 20px !important;
color: #94a3b8 !important;
font-weight: 600;
transition: all 0.3s ease;
white-space: nowrap;
}
ul.nav.nav-pills .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
@@ -412,6 +575,24 @@
color: #fff !important;
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>
<script type="text/javascript">
@@ -423,9 +604,15 @@ $(document).ready(function(){
});
</script>
<style>
/* Mobile Margin Fix */
/* Mobile Margin Fix & Table Scrolling */
@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 {
width: 100% !important; max-width: 100% !important;
padding-left: 4px !important; padding-right: 4px !important;

View File

@@ -1,37 +1,67 @@
{% 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 class="loader-inner">
<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 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 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 class="loader-line-wrap"><div class="loader-line"></div></div>
</div>
</div>
<div id="main_content" style="display: none; opacity: 0; transition: opacity 0.3s ease-in;">
<form id="program_list">
{{ macros.setting_input_text_and_buttons('code', '작품 Code',
[['analysis_btn', '분석'], ['go_anilife_btn', 'Go 애니라이프']], desc='예)
"https://anilife.live/g/l?id=f6e83ec6-bd25-4d6c-9428-c10522687604" 이나 "f6e83ec6-bd25-4d6c-9428-c10522687604"')
}}
</form>
<form id="program_auto_form">
<div id="episode_list"></div>
</form>
</div>
<form id="program_list">
<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;">
<div class="form-group mb-0">
<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 id="program_auto_form">
<div id="episode_list"></div>
</form>
</div>
<!--전체-->
<script src="{{ url_for('.static', filename='js/sjva_ui14.js') }}"></script>
@@ -40,7 +70,7 @@
const package_name = "{{arg['package_name'] }}";
const sub = "{{arg['sub'] }}";
const anilife_url = "{{arg['anilife_url']}}";
{#let current_data = null;#}
// let current_data = null;
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
@@ -133,33 +163,21 @@
episodeList.style.transition = 'opacity 0.3s ease-in';
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 += '&nbsp;&nbsp;&nbsp;&nbsp;<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
str += m_hr_black();
str += m_row_start(0);
tmp = ''
let posterHtml = '';
if (data.image != null) {
// CDN 이미지 프록시 적용
let proxyImgSrc = data.image;
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 += m_row_start(2) + m_col(3, '제목', 'right') + m_col(9, '<strong style="font-size:1.3em;">' + data.title + '</strong>') + m_row_end();
tmp = '';
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 데이터를 각 항목별로 파싱하여 표시
if (data.des1) {
@@ -175,9 +193,22 @@
// 첫 번째 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;
}
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(9, tmp)
str += m_col(12, tmp)
str += m_row_end();
str += '<div class="episode-list-container">';
@@ -185,7 +216,7 @@
// CDN 이미지 프록시 적용
let epThumbSrc = data.episode[i].thumbnail || '';
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">';
@@ -291,25 +322,19 @@
})
$(document).ready(function () {
// DOM 로딩 완료 후 콘텐츠 표시
const mainContent = document.getElementById('main_content');
// DOM 로딩 완료 후 preloader 숨기기
const preloader = document.getElementById('preloader');
// 메인 콘텐츠 보이기 (fade-in 효과)
mainContent.style.display = 'block';
setTimeout(function() {
mainContent.style.opacity = '1';
// preloader 숨기기
if (preloader) {
preloader.style.opacity = '0';
setTimeout(function() {
preloader.style.display = 'none';
}, 300);
}
}, 100);
}, 500);
$("#loader").css("display", 'none');
// console.log({{ arg['code'] }})
});
$("#analysis_btn").unbind("click").bind('click', function (e) {
@@ -452,22 +477,95 @@
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 {
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;
padding: 20px 25px;
padding: 25px;
margin-top: 15px;
line-height: 2.2;
color: #e2e8f0;
border: 1px solid rgba(148, 163, 184, 0.2);
color: #e0e7ff;
border: 1px solid rgba(167, 139, 250, 0.2);
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 {
color: #60a5fa;
color: #a78bfa;
font-weight: 600;
min-width: 100px;
min-width: 90px;
display: inline-block;
}
@@ -485,18 +583,18 @@
align-items: center;
gap: 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: 1px solid rgba(148, 163, 184, 0.12);
border: 1px solid rgba(167, 139, 250, 0.15);
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.episode-card:hover {
background: linear-gradient(135deg, rgba(51, 65, 85, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%);
border-color: rgba(96, 165, 250, 0.5);
background: linear-gradient(135deg, rgba(76, 29, 149, 0.9) 0%, rgba(49, 46, 129, 0.9) 100%);
border-color: rgba(167, 139, 250, 0.5);
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;
border-radius: 5px;
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;
}
@@ -527,7 +625,7 @@
position: absolute;
bottom: 2px;
left: 2px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
font-size: 9px;
font-weight: 700;
@@ -548,7 +646,7 @@
}
.episode-title {
color: #e2e8f0;
color: #e0e7ff;
font-weight: 500;
font-size: 13px;
line-height: 1.3;
@@ -560,7 +658,7 @@
}
.episode-date {
color: #64748b;
color: #a78bfa;
font-size: 11px;
white-space: nowrap;
flex-shrink: 0;
@@ -578,14 +676,84 @@
font-size: 11px;
padding: 3px 10px;
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 {
transform: scale(0.85);
transform: scale(0.9);
width: 60px !important;
height: 25px !important;
min-width: 60px !important;
min-height: 25px !important;
}
/* 모바일 반응형 - Bootstrap 모든 레이아웃 강제 덮어쓰기 */
@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 {
overflow-x: hidden !important;

View File

@@ -180,21 +180,20 @@
str += '<div id="inner_screen_movie" class="row infinite-scroll">';
for (let i in data.anime_list) {
tmp = '<div class="col-6 col-sm-4 col-md-3">';
tmp += '<div class="card">';
tmp = '<div class=\"col-6 col-sm-4 col-md-3\">';
tmp += '<div class=\"card\">';
// 이미지 프록시를 통해 CDN 이미지 로드 (hotlink 보호 우회)
let airingImgUrl = data.anime_list[i].image_link;
if (airingImgUrl && airingImgUrl.includes('cdn.anilife.live')) {
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-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 += '<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?code=' + data.anime_list[i].code + '" class="btn btn-primary cut-text">' + data.anime_list[i].title + '</a>';
tmp += '<div class=\"card-img-container\">';
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 += '<span class=\"episode-badge\">' + data.anime_list[i].epx + '</span>';
tmp += '</div>';
tmp += '<div class=\"card-body\">'
tmp += '<h5 class=\"card-title\">' + data.anime_list[i].title + '</h5>';
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>';
@@ -253,13 +252,12 @@
if (imgUrl && imgUrl.includes('cdn.anilife.live')) {
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 += '<span class="episode-badge">' + data.anime_list[i].epx + '</span>';
tmp += '</div>';
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 += '<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 += '</div>';
tmp += '</div>';
@@ -304,10 +302,14 @@
if (screenImgUrl && screenImgUrl.includes('cdn.anilife.live')) {
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\'" />';
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 += '<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 += '</div>';
tmp += '</div>';
@@ -762,6 +764,27 @@
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 {
padding: 12px !important;
flex-grow: 1;
@@ -854,11 +877,162 @@
}
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 {
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 {
@@ -1055,7 +1229,18 @@ $(document).ready(function(){
<style>
/* Mobile Margin Fix */
@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 {
width: 100% !important; max-width: 100% !important;
padding-left: 4px !important; padding-right: 4px !important;
@@ -1076,6 +1261,12 @@ $(document).ready(function(){
white-space: normal !important; text-align: left !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>
{% endblock %}

View File

@@ -45,7 +45,10 @@
</div>
{{ 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_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;">
@@ -304,6 +307,22 @@
.folder-item.selected {
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>
<script type="text/javascript">
@@ -323,6 +342,22 @@ $('#ani365_auto_make_folder').change(function() {
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){
e.preventDefault();

View File

@@ -546,6 +546,143 @@ $(document).ready(function(){
}
.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; }
/* ========== 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>
<script type="text/javascript">

View File

@@ -1367,6 +1367,58 @@ $(document).ready(function(){
}
.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; }
/* ========== 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>
<script>

View File

@@ -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%);
color: var(--text-color);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: hidden; /* 외부 스크롤 방지 - 흔들림 해결 */
/* overflow: hidden 제거 - 모바일 스크롤 허용 */
}
/* Container & Typography */
@@ -29,6 +29,10 @@
}
@media (max-width: 768px) {
body {
overflow-x: hidden !important;
overflow-y: auto !important; /* 세로 스크롤 허용 */
}
.container-fluid {
padding: 4px; /* 모바일 더 작은 여백 */
}
@@ -39,6 +43,12 @@
margin-top: 8px;
border-radius: 6px;
}
/* 로그 테이블 뷰포트 기반 높이 */
textarea#log, textarea#add {
max-height: 60vh !important;
height: auto !important;
min-height: 300px !important;
}
}
h1, h2, h3, h4, h5, h6 {

View File

@@ -1071,5 +1071,118 @@ $(document).ready(function(){
overflow: auto !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>
{% endblock %}

View File

@@ -1376,6 +1376,58 @@ $(document).ready(function(){
}
.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; }
/* ========== 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>
<script>