Compare commits
3 Commits
c38a7ae39b
...
1cdf68cc59
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cdf68cc59 | |||
| 9d5d1514c4 | |||
| 9c4f36de6b |
10
README.md
10
README.md
@@ -3,9 +3,13 @@
|
|||||||
FlaskFarm용 범용 다운로드 매니저 플러그인입니다.
|
FlaskFarm용 범용 다운로드 매니저 플러그인입니다.
|
||||||
여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다.
|
여러 다운로더 플러그인(YouTube, Anime 등)의 다운로드 요청을 통합 관리하고 큐(Queue)를 제공합니다.
|
||||||
|
|
||||||
## v0.2.27 변경사항 (2026-01-09)
|
## v0.2.30 변경사항 (2026-01-12)
|
||||||
- **자가 업데이트 기능 추가**: 설정 페이지에서 "Update" 버튼 클릭으로 Git Pull 및 플러그인 핫 리로드 지원
|
- **자막 자동 다운로드 및 변환**: `ytdlp_aria2` 다운로더에 VTT 자막 다운로드 및 SRT 자동 변환 로직 내장.
|
||||||
- **버전 체크 API**: GitHub에서 최신 버전 정보를 가져와 업데이트 알림 표시 (1시간 캐싱)
|
- **경로 정규화 강화**: `output_template` 생성 시 중복 구분자(`//`, `\.\`)를 제거하여 경로 오염 방지.
|
||||||
|
- **다운로드 완료 지점 최적화**: 비디오 다운로드 성공 직후 자막 처리가 이어지도록 흐름 개선.
|
||||||
|
|
||||||
|
## v0.2.29 변경사항 (2026-01-11)
|
||||||
|
- **Anilife 고유 ID 지원**: 에피소드 고유 코드가 없는 경우 제목 기반 매칭 로직 보강.
|
||||||
|
|
||||||
## v0.2.24 변경사항 (2026-01-08)
|
## v0.2.24 변경사항 (2026-01-08)
|
||||||
- **Chrome 확장프로그램 추가**: YouTube에서 GDM으로 바로 다운로드 전송
|
- **Chrome 확장프로그램 추가**: YouTube에서 GDM으로 바로 다운로드 전송
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class FfmpegHlsDownloader(BaseDownloader):
|
|||||||
if not filename:
|
if not filename:
|
||||||
filename = f"download_{int(__import__('time').time())}.mp4"
|
filename = f"download_{int(__import__('time').time())}.mp4"
|
||||||
|
|
||||||
filepath = os.path.join(save_path, filename)
|
filepath = os.path.abspath(os.path.join(save_path, filename))
|
||||||
|
filepath = os.path.normpath(filepath)
|
||||||
|
|
||||||
# ffmpeg 명령어 구성
|
# ffmpeg 명령어 구성
|
||||||
ffmpeg_path = options.get('ffmpeg_path', 'ffmpeg')
|
ffmpeg_path = options.get('ffmpeg_path', 'ffmpeg')
|
||||||
@@ -173,7 +174,16 @@ class FfmpegHlsDownloader(BaseDownloader):
|
|||||||
"""다운로드 취소"""
|
"""다운로드 취소"""
|
||||||
super().cancel()
|
super().cancel()
|
||||||
if self._process:
|
if self._process:
|
||||||
self._process.terminate()
|
try:
|
||||||
|
# [FIX] 파이프 명시적으로 닫기
|
||||||
|
if self._process.stdout: self._process.stdout.close()
|
||||||
|
if self._process.stderr: self._process.stderr.close()
|
||||||
|
|
||||||
|
self._process.terminate()
|
||||||
|
# 짧은 대기 후 여전히 살아있으면 kill
|
||||||
|
try: self._process.wait(timeout=1)
|
||||||
|
except: self._process.kill()
|
||||||
|
except: pass
|
||||||
|
|
||||||
def _get_duration(self, url: str, ffprobe_path: str, headers: Dict) -> float:
|
def _get_duration(self, url: str, ffprobe_path: str, headers: Dict) -> float:
|
||||||
"""ffprobe로 영상 길이 획득"""
|
"""ffprobe로 영상 길이 획득"""
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ class HttpDirectDownloader(BaseDownloader):
|
|||||||
if not filename:
|
if not filename:
|
||||||
filename = url.split('/')[-1].split('?')[0] or f"download_{int(__import__('time').time())}"
|
filename = url.split('/')[-1].split('?')[0] or f"download_{int(__import__('time').time())}"
|
||||||
|
|
||||||
filepath = os.path.join(save_path, filename)
|
filepath = os.path.abspath(os.path.join(save_path, filename))
|
||||||
|
filepath = os.path.normpath(filepath)
|
||||||
|
|
||||||
# 헤더 설정
|
# 헤더 설정
|
||||||
headers = options.get('headers', {})
|
headers = options.get('headers', {})
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
try:
|
try:
|
||||||
os.makedirs(save_path, exist_ok=True)
|
os.makedirs(save_path, exist_ok=True)
|
||||||
|
|
||||||
# 출력 템플릿
|
# 출력 템플릿 (outtmpl 옵션 우선 처리)
|
||||||
if filename:
|
raw_outtmpl = options.get('outtmpl') or filename or '%(title)s.%(ext)s'
|
||||||
output_template = os.path.join(save_path, filename)
|
|
||||||
else:
|
|
||||||
output_template = os.path.join(save_path, '%(title)s.%(ext)s')
|
|
||||||
|
|
||||||
|
# 경로와 템플릿 결합 후 정규화
|
||||||
|
output_template = os.path.abspath(os.path.join(save_path, raw_outtmpl))
|
||||||
|
# 윈도우/리눅스 구분 없이 중복 슬래시 제거 및 절대 경로 확보
|
||||||
|
output_template = os.path.normpath(output_template)
|
||||||
|
|
||||||
# yt-dlp 명령어 구성
|
# yt-dlp 명령어 구성
|
||||||
cmd = [
|
cmd = [
|
||||||
'yt-dlp',
|
'yt-dlp',
|
||||||
@@ -58,6 +60,10 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
cmd.extend(['--print', 'before_dl:GDM_FIX:title:%(title)s'])
|
cmd.extend(['--print', 'before_dl:GDM_FIX:title:%(title)s'])
|
||||||
cmd.extend(['--print', 'before_dl:GDM_FIX:thumb:%(thumbnail)s'])
|
cmd.extend(['--print', 'before_dl:GDM_FIX:thumb:%(thumbnail)s'])
|
||||||
|
|
||||||
|
# 속도 제한 설정
|
||||||
|
max_rate = P.ModelSetting.get('max_download_rate')
|
||||||
|
rate_limited = bool(max_rate and max_rate != '0')
|
||||||
|
|
||||||
# aria2c 사용 (설치되어 있으면)
|
# aria2c 사용 (설치되어 있으면)
|
||||||
aria2c_path = options.get('aria2c_path', 'aria2c')
|
aria2c_path = options.get('aria2c_path', 'aria2c')
|
||||||
connections = options.get('connections', 4)
|
connections = options.get('connections', 4)
|
||||||
@@ -65,21 +71,18 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if self._check_aria2c(aria2c_path):
|
if self._check_aria2c(aria2c_path):
|
||||||
cmd.extend(['--external-downloader', aria2c_path])
|
cmd.extend(['--external-downloader', aria2c_path])
|
||||||
# aria2c 설정: -x=연결수, -s=분할수, -j=병렬, -k=조각크기, --console-log-level=notice로 진행률 출력
|
# aria2c 설정: -x=연결수, -s=분할수, -j=병렬, -k=조각크기, --console-log-level=notice로 진행률 출력
|
||||||
cmd.extend(['--external-downloader-args', f'aria2c:-x{connections} -s{connections} -j{connections} -k1M --summary-interval=1 --console-log-level=notice'])
|
aria2_args = f'aria2c:-x{connections} -s{connections} -j{connections} -k1M --summary-interval=1 --console-log-level=notice'
|
||||||
|
if rate_limited:
|
||||||
|
aria2_args = f'{aria2_args} --max-download-limit={max_rate}'
|
||||||
|
cmd.extend(['--external-downloader-args', aria2_args])
|
||||||
logger.info(f'[GDM] Using aria2c for multi-threaded download (connections: {connections})')
|
logger.info(f'[GDM] Using aria2c for multi-threaded download (connections: {connections})')
|
||||||
|
|
||||||
# 진행률 템플릿 추가 (yt-dlp native downloader)
|
# 진행률 템플릿 추가 (yt-dlp native downloader)
|
||||||
cmd.extend(['--progress-template', 'download:GDM_PROGRESS:%(progress._percent_str)s:%(progress._speed_str)s:%(progress._eta_str)s'])
|
cmd.extend(['--progress-template', 'download:GDM_PROGRESS:%(progress._percent_str)s:%(progress._speed_str)s:%(progress._eta_str)s'])
|
||||||
|
|
||||||
# 속도 제한 설정
|
# yt-dlp native downloader 제한 (external-downloader 미사용/보조 경로)
|
||||||
max_rate = P.ModelSetting.get('max_download_rate')
|
if rate_limited:
|
||||||
if max_rate == '0':
|
cmd.extend(['--limit-rate', max_rate])
|
||||||
max_rate_arg = ''
|
|
||||||
log_rate_msg = '무제한'
|
|
||||||
else:
|
|
||||||
max_rate_arg = f'--max-download-limit={max_rate}'
|
|
||||||
log_rate_msg = max_rate
|
|
||||||
cmd.extend(['--limit-rate', max_rate]) # Native downloader limit
|
|
||||||
|
|
||||||
# 포맷 선택
|
# 포맷 선택
|
||||||
format_spec = options.get('format')
|
format_spec = options.get('format')
|
||||||
@@ -147,14 +150,6 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if options.get('add_metadata'):
|
if options.get('add_metadata'):
|
||||||
cmd.append('--add-metadata')
|
cmd.append('--add-metadata')
|
||||||
|
|
||||||
if options.get('outtmpl'):
|
|
||||||
# outtmpl 옵션이 별도로 전달된 경우 덮어쓰기 (output_template는 -o가 이미 차지함)
|
|
||||||
# 하지만 yt-dlp -o 옵션이 곧 outtmpl임.
|
|
||||||
# 파일명 템플릿 문제 해결을 위해 filename 인자 대신 outtmpl 옵션을 우선시
|
|
||||||
# 위에서 -o output_template를 이미 넣었으므로, 여기서 다시 넣으면 중복될 수 있음.
|
|
||||||
# 따라서 로직 수정: filename 없이 outtmpl만 온 경우
|
|
||||||
pass
|
|
||||||
|
|
||||||
# URL 추가
|
# URL 추가
|
||||||
cmd.append(url)
|
cmd.append(url)
|
||||||
|
|
||||||
@@ -267,6 +262,15 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
if self._process.returncode == 0:
|
if self._process.returncode == 0:
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(100, '', '')
|
progress_callback(100, '', '')
|
||||||
|
|
||||||
|
# 자막 다운로드 처리
|
||||||
|
vtt_url = options.get('subtitles')
|
||||||
|
if vtt_url and final_filepath:
|
||||||
|
try:
|
||||||
|
self._download_subtitle(vtt_url, final_filepath, headers=options.get('headers'))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[GDM] Subtitle download error: {e}')
|
||||||
|
|
||||||
return {'success': True, 'filepath': final_filepath}
|
return {'success': True, 'filepath': final_filepath}
|
||||||
else:
|
else:
|
||||||
return {'success': False, 'error': f'Exit code: {self._process.returncode}'}
|
return {'success': False, 'error': f'Exit code: {self._process.returncode}'}
|
||||||
@@ -305,7 +309,16 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
"""다운로드 취소"""
|
"""다운로드 취소"""
|
||||||
super().cancel()
|
super().cancel()
|
||||||
if self._process:
|
if self._process:
|
||||||
self._process.terminate()
|
try:
|
||||||
|
# [FIX] 파이프 명시적으로 닫기
|
||||||
|
if self._process.stdout: self._process.stdout.close()
|
||||||
|
if self._process.stderr: self._process.stderr.close()
|
||||||
|
|
||||||
|
self._process.terminate()
|
||||||
|
# 짧은 대기 후 여전히 살아있으면 kill
|
||||||
|
try: self._process.wait(timeout=1)
|
||||||
|
except: self._process.kill()
|
||||||
|
except: pass
|
||||||
|
|
||||||
def _check_aria2c(self, aria2c_path: str) -> bool:
|
def _check_aria2c(self, aria2c_path: str) -> bool:
|
||||||
"""aria2c 설치 확인"""
|
"""aria2c 설치 확인"""
|
||||||
@@ -318,3 +331,57 @@ class YtdlpAria2Downloader(BaseDownloader):
|
|||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _download_subtitle(self, vtt_url: str, output_path: str, headers: Optional[dict] = None):
|
||||||
|
"""자막 다운로드 및 SRT 변환"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
# 자막 파일 경로 생성 (비디오 파일명.srt)
|
||||||
|
video_basename = os.path.splitext(output_path)[0]
|
||||||
|
srt_path = video_basename + ".srt"
|
||||||
|
|
||||||
|
logger.info(f"[GDM] Downloading subtitle from: {vtt_url}")
|
||||||
|
response = requests.get(vtt_url, headers=headers, timeout=30)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
vtt_content = response.text
|
||||||
|
srt_content = self._vtt_to_srt(vtt_content)
|
||||||
|
with open(srt_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(srt_content)
|
||||||
|
logger.info(f"[GDM] Subtitle saved to: {srt_path}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[GDM] Failed to download subtitle: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _vtt_to_srt(self, vtt_content: str) -> str:
|
||||||
|
"""VTT 형식을 SRT 형식으로 간단히 변환"""
|
||||||
|
if not vtt_content.startswith("WEBVTT"):
|
||||||
|
return vtt_content
|
||||||
|
|
||||||
|
lines = vtt_content.split("\n")
|
||||||
|
srt_lines = []
|
||||||
|
cue_index = 1
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith("WEBVTT") or line.startswith("NOTE") or line.startswith("STYLE"):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if not line:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if "-->" in line:
|
||||||
|
# VTT 타임코드를 SRT 형식으로 변환 (. -> ,)
|
||||||
|
srt_timecode = line.replace(".", ",")
|
||||||
|
srt_lines.append(str(cue_index))
|
||||||
|
srt_lines.append(srt_timecode)
|
||||||
|
cue_index += 1
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and lines[i].strip():
|
||||||
|
srt_lines.append(lines[i].rstrip())
|
||||||
|
i += 1
|
||||||
|
srt_lines.append("")
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
return "\n".join(srt_lines)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
title: "GDM"
|
title: "GDM"
|
||||||
package_name: gommi_downloader_manager
|
package_name: gommi_downloader_manager
|
||||||
version: '0.2.28'
|
version: '0.2.33'
|
||||||
description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원
|
description: FlaskFarm 범용 다운로더 큐 - YouTube, 애니24, 링크애니, Anilife 지원
|
||||||
developer: projectdx
|
developer: projectdx
|
||||||
home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager
|
home: https://gitea.yommi.duckdns.org/projectdx/gommi_downloader_manager
|
||||||
|
|||||||
28
mod_queue.py
28
mod_queue.py
@@ -297,13 +297,13 @@ class ModuleQueue(PluginModuleBase):
|
|||||||
|
|
||||||
# 1. Git Pull
|
# 1. Git Pull
|
||||||
cmd = ['git', '-C', plugin_path, 'pull']
|
cmd = ['git', '-C', plugin_path, 'pull']
|
||||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||||
stdout, stderr = process.communicate()
|
|
||||||
|
|
||||||
if process.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise Exception(f"Git pull 실패: {stderr}")
|
raise Exception(f"Git pull 실패: {result.stderr}")
|
||||||
|
|
||||||
self.P.logger.info(f"Git pull 결과: {stdout}")
|
self.P.logger.info(f"Git pull 결과: {result.stdout}")
|
||||||
|
stdout = result.stdout
|
||||||
|
|
||||||
# 2. 모듈 리로드 (Hot-Reload)
|
# 2. 모듈 리로드 (Hot-Reload)
|
||||||
self.reload_plugin()
|
self.reload_plugin()
|
||||||
@@ -890,7 +890,19 @@ class DownloadTask:
|
|||||||
else:
|
else:
|
||||||
modules = []
|
modules = []
|
||||||
|
|
||||||
|
# 모듈명 추출 (예: anime_downloader_linkkf -> linkkf)
|
||||||
|
target_module_name = None
|
||||||
|
if len(parts) > 1:
|
||||||
|
target_module_name = parts[-1]
|
||||||
|
|
||||||
for module_name, module_instance in modules:
|
for module_name, module_instance in modules:
|
||||||
|
# 모듈 인스턴스의 name 또는 변수명 확인
|
||||||
|
instance_name = getattr(module_instance, 'name', module_name)
|
||||||
|
|
||||||
|
# 대상 모듈명이 지정되어 있으면 일치하는 경우에만 호출
|
||||||
|
if target_module_name and instance_name != target_module_name:
|
||||||
|
continue
|
||||||
|
|
||||||
if hasattr(module_instance, 'plugin_callback'):
|
if hasattr(module_instance, 'plugin_callback'):
|
||||||
callback_data = {
|
callback_data = {
|
||||||
'callback_id': self.callback_id,
|
'callback_id': self.callback_id,
|
||||||
@@ -901,8 +913,10 @@ class DownloadTask:
|
|||||||
}
|
}
|
||||||
module_instance.plugin_callback(callback_data)
|
module_instance.plugin_callback(callback_data)
|
||||||
callback_invoked = True
|
callback_invoked = True
|
||||||
P.logger.info(f"Callback invoked on module {module_name}")
|
P.logger.info(f"Callback invoked on module {instance_name}")
|
||||||
break
|
# 대상 모듈을 명확히 찾았으면 종료
|
||||||
|
if target_module_name:
|
||||||
|
break
|
||||||
|
|
||||||
if not callback_invoked:
|
if not callback_invoked:
|
||||||
P.logger.debug(f"No plugin_callback method found in {self.caller_plugin}")
|
P.logger.debug(f"No plugin_callback method found in {self.caller_plugin}")
|
||||||
|
|||||||
@@ -46,7 +46,63 @@
|
|||||||
background-color: #3e5770 !important;
|
background-color: #3e5770 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loading { display: none !important; }
|
/* Plugin-owned loading override (independent from Flaskfarm default loader assets) */
|
||||||
|
#loading,
|
||||||
|
#modal_loading {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3000010 !important;
|
||||||
|
background: rgba(10, 22, 36, 0.46);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading img,
|
||||||
|
#modal_loading img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading::before,
|
||||||
|
#modal_loading::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
margin-left: -29px;
|
||||||
|
margin-top: -29px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.24);
|
||||||
|
border-top-color: var(--accent-primary);
|
||||||
|
border-right-color: var(--accent-secondary);
|
||||||
|
animation: gdm-loader-spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading::after,
|
||||||
|
#modal_loading::after {
|
||||||
|
content: "LOADING";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: calc(50% + 44px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading[style*="display: block"],
|
||||||
|
#loading[style*="display: inline-block"],
|
||||||
|
#modal_loading[style*="display: block"],
|
||||||
|
#modal_loading[style*="display: inline-block"] {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gdm-loader-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
#gommi_download_manager_queue_list {
|
#gommi_download_manager_queue_list {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
@@ -143,6 +199,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile 5px Padding - Maximum Screen Usage */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container, .container-fluid, #main_container, #gommi_download_manager_queue_list {
|
||||||
|
padding-left: 5px !important;
|
||||||
|
padding-right: 5px !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
[class*="col-"] {
|
||||||
|
padding-left: 4px !important;
|
||||||
|
padding-right: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -740,6 +814,65 @@
|
|||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== CUSTOM CROSSHAIR CURSOR ===== */
|
||||||
|
.custom-cursor-outer {
|
||||||
|
position: fixed;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: rgba(168, 85, 247, 0.25);
|
||||||
|
border: none;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 99999;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.15s ease, height 0.15s ease, background 0.15s ease, transform 0.15s ease, filter 0.15s ease;
|
||||||
|
/* 5-point star clip-path */
|
||||||
|
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
|
||||||
|
/* Strong outline via multiple drop-shadows */
|
||||||
|
filter:
|
||||||
|
drop-shadow(0 0 0 rgba(168, 85, 247, 1))
|
||||||
|
drop-shadow(1px 0 0 rgba(168, 85, 247, 1))
|
||||||
|
drop-shadow(-1px 0 0 rgba(168, 85, 247, 1))
|
||||||
|
drop-shadow(0 1px 0 rgba(168, 85, 247, 1))
|
||||||
|
drop-shadow(0 -1px 0 rgba(168, 85, 247, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-cursor-dot {
|
||||||
|
position: fixed;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(168, 85, 247, 1);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 99999;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: transform 0.08s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cursor hover state on interactive elements */
|
||||||
|
.custom-cursor-outer.hovering {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
background: rgba(192, 132, 252, 0.5);
|
||||||
|
transform: translate(-50%, -50%) rotate(36deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-cursor-dot.hovering {
|
||||||
|
transform: translate(-50%, -50%) scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default cursor when custom cursor is active */
|
||||||
|
#gommi_download_manager_queue_list,
|
||||||
|
#gommi_download_manager_queue_list * {
|
||||||
|
cursor: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exception: keep pointer on buttons for accessibility hint */
|
||||||
|
#gommi_download_manager_queue_list button:hover,
|
||||||
|
#gommi_download_manager_queue_list .dl-btn:hover,
|
||||||
|
#gommi_download_manager_queue_list .btn-premium:hover {
|
||||||
|
cursor: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="gommi_download_manager_queue_list" class="mt-4">
|
<div id="gommi_download_manager_queue_list" class="mt-4">
|
||||||
@@ -767,6 +900,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Cursor Elements -->
|
||||||
|
<div class="custom-cursor-outer" id="cursor-ring"></div>
|
||||||
|
<div class="custom-cursor-dot" id="cursor-dot"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ===== CUSTOM CURSOR WITH INERTIA EFFECT (Optimized) =====
|
||||||
|
(function() {
|
||||||
|
const ring = document.getElementById('cursor-ring');
|
||||||
|
const dot = document.getElementById('cursor-dot');
|
||||||
|
|
||||||
|
if (!ring || !dot) return;
|
||||||
|
|
||||||
|
let mouseX = 0, mouseY = 0;
|
||||||
|
let ringX = 0, ringY = 0;
|
||||||
|
let dotX = 0, dotY = 0;
|
||||||
|
|
||||||
|
// Interactive elements selector
|
||||||
|
const interactiveSelector = 'a, button, .btn, .dl-btn, .btn-premium, .dl-card, input, select';
|
||||||
|
|
||||||
|
// Track mouse position
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
mouseX = e.clientX;
|
||||||
|
mouseY = e.clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lerp animation with smooth follow
|
||||||
|
function animate() {
|
||||||
|
// Ring follows with inertia
|
||||||
|
ringX += (mouseX - ringX) * 0.12;
|
||||||
|
ringY += (mouseY - ringY) * 0.12;
|
||||||
|
ring.style.left = ringX + 'px';
|
||||||
|
ring.style.top = ringY + 'px';
|
||||||
|
|
||||||
|
// Dot follows quickly
|
||||||
|
dotX += (mouseX - dotX) * 0.25;
|
||||||
|
dotY += (mouseY - dotY) * 0.25;
|
||||||
|
dot.style.left = dotX + 'px';
|
||||||
|
dot.style.top = dotY + 'px';
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Event delegation for hover effects (document-wide)
|
||||||
|
document.addEventListener('mouseover', (e) => {
|
||||||
|
if (e.target.matches(interactiveSelector) || e.target.closest(interactiveSelector)) {
|
||||||
|
ring.classList.add('hovering');
|
||||||
|
dot.classList.add('hovering');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseout', (e) => {
|
||||||
|
if (e.target.matches(interactiveSelector) || e.target.closest(interactiveSelector)) {
|
||||||
|
ring.classList.remove('hovering');
|
||||||
|
dot.classList.remove('hovering');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show cursor on page
|
||||||
|
ring.style.opacity = '1';
|
||||||
|
dot.style.opacity = '1';
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// PACKAGE_NAME and MODULE_NAME are already defined globally by framework
|
// PACKAGE_NAME and MODULE_NAME are already defined globally by framework
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,64 @@
|
|||||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plugin-owned loading override (independent from Flaskfarm default loader assets) */
|
||||||
|
#loading,
|
||||||
|
#modal_loading {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3000010 !important;
|
||||||
|
background: rgba(7, 16, 35, 0.46);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading img,
|
||||||
|
#modal_loading img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading::before,
|
||||||
|
#modal_loading::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
margin-left: -29px;
|
||||||
|
margin-top: -29px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.24);
|
||||||
|
border-top-color: var(--accent-primary);
|
||||||
|
border-right-color: var(--accent-secondary);
|
||||||
|
animation: gdm-loader-spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading::after,
|
||||||
|
#modal_loading::after {
|
||||||
|
content: "LOADING";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: calc(50% + 44px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading[style*="display: block"],
|
||||||
|
#loading[style*="display: inline-block"],
|
||||||
|
#modal_loading[style*="display: block"],
|
||||||
|
#modal_loading[style*="display: inline-block"] {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gdm-loader-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
#gommi_download_manager_queue_setting {
|
#gommi_download_manager_queue_setting {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
@@ -237,6 +295,10 @@
|
|||||||
<option value="1M" {% if arg['max_download_rate'] == '1M' %}selected{% endif %}>1 MB/s</option>
|
<option value="1M" {% if arg['max_download_rate'] == '1M' %}selected{% endif %}>1 MB/s</option>
|
||||||
<option value="3M" {% if arg['max_download_rate'] == '3M' %}selected{% endif %}>3 MB/s</option>
|
<option value="3M" {% if arg['max_download_rate'] == '3M' %}selected{% endif %}>3 MB/s</option>
|
||||||
<option value="5M" {% if arg['max_download_rate'] == '5M' %}selected{% endif %}>5 MB/s</option>
|
<option value="5M" {% if arg['max_download_rate'] == '5M' %}selected{% endif %}>5 MB/s</option>
|
||||||
|
<option value="6M" {% if arg['max_download_rate'] == '6M' %}selected{% endif %}>6 MB/s</option>
|
||||||
|
<option value="7M" {% if arg['max_download_rate'] == '7M' %}selected{% endif %}>7 MB/s</option>
|
||||||
|
<option value="8M" {% if arg['max_download_rate'] == '8M' %}selected{% endif %}>8 MB/s</option>
|
||||||
|
<option value="9M" {% if arg['max_download_rate'] == '9M' %}selected{% endif %}>9 MB/s</option>
|
||||||
<option value="10M" {% if arg['max_download_rate'] == '10M' %}selected{% endif %}>10 MB/s</option>
|
<option value="10M" {% if arg['max_download_rate'] == '10M' %}selected{% endif %}>10 MB/s</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user