Initial commit
This commit is contained in:
0
central_init_restrictions_checker.py
Normal file
0
central_init_restrictions_checker.py
Normal file
0
commands.py
Normal file
0
commands.py
Normal file
54
config/configuration.ini
Normal file
54
config/configuration.ini
Normal file
@@ -0,0 +1,54 @@
|
||||
# =========================
|
||||
# CONFIGURATION
|
||||
# =========================
|
||||
|
||||
|
||||
|
||||
[DIRECT_SETTINGS]
|
||||
BOT_USERNAME = "blood_linksbot" # the username of your Telegram bot (without @)
|
||||
BOT_TOKEN = "8649631256:AAHDQgDhpd8ymxACdNni1HomLuMzcm_W2Lg" # the token for your Telegram bot (get it from @BotFather), cannot be changed using commands and completely hidden for security reasons
|
||||
TELEGRAM_PUBLIC_GROUP_ID = -1003571704137 # the ID of the public group where user-uploaded content will be exposed (if chosen)
|
||||
TELEGRAM_ADMINISTRATION_GROUP_ID = -1003737694563 # the ID of the private group where admin-uploaded content will be reviewed/generated
|
||||
OWNER_TELEGRAM_USER_ID = 8615679055 # the Telegram user ID of the bot owner (for managing critical settings), cannot be changed using commands
|
||||
|
||||
|
||||
[BOT_SETTINGS]
|
||||
ALLOW_LINKS_TO_TEXT = true # whether to allow users to link their text messages (true = feature enabled & allowed, false = feature disabled & not allowed)
|
||||
ALLOW_LINKS_TO_MEDIA = true # whether to allow users to link their media from messages personally and receive custom links (true = feature enabled & allowed, false = feature disabled & not allowed)
|
||||
WARNING_MESSAGE_ENABLED = true # whether to require user to confirm rules/ usage plan before interacting with the bot, true = enabled, false = disabled
|
||||
MAXIMUM_CHAR_LIMIT_FOR_TEXT_MSGS = 9000 # the maximum character limit for text messages that users can link, set to a reasonable number to prevent abuse (e.g., 12000 characters)
|
||||
REPORT_FEATURE_ENABLED = true # whether to allow users to report inappropriate content, true = enabled, false = disabled
|
||||
DELETE_TELEGRAM_ACTIONS = true # whether to delete all basic telegram report messages/warnings such as user left, joined, message x was pinned, ect. in the groups to keep them clean, true = enabled, false = disabled
|
||||
ALLOW_USERS_TO_DELETE_CONTENT = true # whether to allow users to delete their own content/links, making all links/content ID's immediately ineffective, true = enabled, false = disabled
|
||||
ACTUALLY_DELETE_CONTENT = false # THIS OPTION IS NOT SHOWN; whether to actually the content that gets burned, deleted by users or administrators, true = fully deletes all media if that was the intended action & only saves content after accepting submissions/when neccessary, false = saves ALL contents, regardless whether they were accepted by administrators or not & does not delete them under all circumstances
|
||||
DELETE_SUBMISSIONS_AFTER_REVIEW = moderated # whether to delete the original Telegram messages of user submissions after they have been reviewed by administrators, true = enabled, false = disabled, moderated = only delete if the content was moderated directly, i.e. the user banned for posting it
|
||||
ALLOW_CUSTOM_PASSWORDS_FOR_LINKS = false # whether to allow users to choose their own custom passwords for accessing their content links, true = enabled, false = disabled (if disabled, the bot will generate random passwords for all links)
|
||||
# if chosen direct, a randomized string is generated and directly embedded within URL for automatic loading, if chosen external the user has to enter the password given manually before accessing contents, to avoid long, complex strings uses a wordlist of possible passwords and chooses randomly from it
|
||||
PASSWORD_METHOD_TYPE = "direct" # options: "direct", "external"
|
||||
EXTERNAL_PASSWORD_LIST_TXT = "./resources/password_access_list.txt"
|
||||
|
||||
|
||||
[STORAGE_SETTINGS]
|
||||
TEXT_STORAGE_TYPE = "sqlite3" # options: "sqlite3", "json"
|
||||
BAN_LOG_STORAGE_TYPE = "sqlite3" # options: "sqlite3", "json"
|
||||
FILE_HASHES_KEY_WRAPPED_STORAGE_TYPE = "sqlite3" # options: "sqlite3", "json"
|
||||
PREVIOUS_USERS_FOR_ADVERTISEMENTS_STORAGE_TYPE = "sqlite3" # options: "sqlite3", "json"
|
||||
USER_UPLOADED_CONTENT_EXPOSED_METHOD = "web" # options: "web", "telegram", "both"
|
||||
ADMIN_UPLOADED_CONTENT_EXPOSED_METHOD = "web" # options: "web", "telegram", "both"
|
||||
# DO NOT ENTER ANYTHING HERE MANUALLY, SOFTWARE SETS ENCRYPTION KEYS AUTOMATICALLY
|
||||
EPHEMERAL_AES_ENVELOPE_KEY = "" # This feature uses xchacha20-poly1305 alongside AES-KW (RFC 5649) for wrapping chacha keys - randomized wrapper key to securely encrypt/decrypt every file/textblob from traffic, cannot be changed using commands and completely hidden for security reasons
|
||||
SUPPORTED_MEDIA_TYPES_STRING = "image, video, audio, document" # the media types that are allowed to be linked by users, will show up as 'X is not an allowed media type, only the following media types are allowed: X, Y, Z' if user tries to link a media type that is not in this list, shown but cannot be changed using commands
|
||||
DATABASE_FOLDER_PATH = "./databases" # the folder path where all databases (if using sqlite3) or json files (if using json) will be stored, make sure the bot has read/write permissions for this folder, shown yet cannot be changed using commands
|
||||
|
||||
|
||||
[WEB_SETTINGS]
|
||||
PORT_EXPOSED = 9465 # the port that will be exposed for the web server (if enabled)
|
||||
GENERATE_SECURE_KEY_FOR_CONTENT_LINKS = true # whether to generate secure, random keys for accessing web content instead of allowing direct API access to anyone, true = enabled, false = disabled
|
||||
ALLOW_COMMENTS_ON_WEB_CONTENT_PAGES = true # whether to allow users to comment anonymously (cloudflare-integration important!) on content pages on the web interface, true = enabled, false = disabled
|
||||
|
||||
|
||||
[MISC]
|
||||
DEVELOPER_TELEGRAM_USERNAME = "forgecadrape" # credits for making this bot (plain Username without @), cannot be changed using commands
|
||||
FORBIDDEN_CONTENT_TYPES_STRING = "CP, necriphilia, ect." # will shows up as 'X is strictly forbidden and leads to permanent deletion of links as well as exclusion from bot.'
|
||||
MEDIA_TOPIC_FOR_BOT = "gore" # the topic that is generally seen and accepted by administrators for media generation, crucial for explicitly stating to new users which content will likely get approved.
|
||||
CONTACT_TELEGERAM_LINK = "harmfulmeowbot?start=submit" # can be either BOT_NAME?start=submit OR TELEGRAM_USERNAME
|
||||
0
configloader.py
Normal file
0
configloader.py
Normal file
0
cryptographic_methods.py
Normal file
0
cryptographic_methods.py
Normal file
0
db_handler.py
Normal file
0
db_handler.py
Normal file
0
db_init.py
Normal file
0
db_init.py
Normal file
62
main.py
Normal file
62
main.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from aiogram import Bot, Dispatcher, F
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
import asyncio
|
||||
|
||||
bot = Bot(
|
||||
token=BOT_TOKEN,
|
||||
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||
)
|
||||
|
||||
dp = Dispatcher()
|
||||
|
||||
def is_admin(user_id: int) -> bool:
|
||||
return user_id in ADMINS
|
||||
|
||||
@dp.message(Command("help"))
|
||||
async def help_command(message: Message):
|
||||
|
||||
if not is_admin(message.from_user.id):
|
||||
return
|
||||
|
||||
help_text = """
|
||||
<b>Bold text</b>
|
||||
|
||||
<i>Italic text</i>
|
||||
|
||||
<u>Underlined text</u>
|
||||
|
||||
<s>Strikethrough text</s>
|
||||
|
||||
<tg-spoiler>Spoiler text</tg-spoiler>
|
||||
|
||||
<code>Mono font / inline code</code>
|
||||
|
||||
<pre>
|
||||
Multi-line
|
||||
mono block
|
||||
</pre>
|
||||
|
||||
<b><i>Bold + Italic</i></b>
|
||||
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
|
||||
<a href="https://telegram.org">Clickable URL</a>
|
||||
"""
|
||||
|
||||
await message.answer(help_text)
|
||||
|
||||
@dp.message(Command("start"))
|
||||
async def start_command(message: Message):
|
||||
await message.answer("Bot is running.")
|
||||
|
||||
async def main():
|
||||
print("Bot started.")
|
||||
await dp.start_polling(bot)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
70
msic_bot.py
Normal file
70
msic_bot.py
Normal file
@@ -0,0 +1,70 @@
|
||||
HELP_MESSAGE = f"""<b><u>t.me/{BOT_USERNAME}</u></b>
|
||||
|
||||
<b>--- ADMINISTRATION CMDS ---</b>
|
||||
<code>/add_admin</code> ID or reply to msg - adds an administrator which provides them with the ability of interacting with the bot in administrative ways
|
||||
<code>/rm_admin</code> ID or reply to msg - removes an administrator which restricts them from interacting with the bot in any administrative way
|
||||
<code>/add_coown</code> ID or reply to msg - adds a co-owner which have nearly full permissions over the bot and also the ability to edit configurations
|
||||
<code>/rm_coown</code> ID or reply to msg - removes a co-owner which have nearly full permissions over the bot and also the ability to edit configurations
|
||||
|
||||
(An Owner/Co-Owner can add other Administrators.)
|
||||
|
||||
<b>--- LINK/USE CMDS ---</b>
|
||||
<code>/create_link</code> reply to msg with vids/pics /submit in dms and approve by yourself - generates a simple link users can click one to view your uploaded media.
|
||||
<code>/create_full_link</code> - takes all attachments of this channel and generates link to it, use carefully, high overload potential + pulls all media, even if not gore.
|
||||
<code>/adv_group</code> TG_INV_LINK - sends a telegram group link provided to all users who previously interacted with the bot
|
||||
<code>/help</code> - shows this msg
|
||||
|
||||
<b>--- LINK MISC CMDS ---</b>
|
||||
<code>/show_links</code> - shows all links
|
||||
<code>/del_link</code> LINK_ID - deletes a link by link id from /show_links
|
||||
<code>/push_web</code> WEB_URL ID or reply to msg - adds content replied to/referenced to the already existing web accessor provided
|
||||
|
||||
<b>--- CONFIG CMDS ---</b>
|
||||
<code>/config show</code> - shows the current configuration alongside keys & all values except for the bot token, which is hidden for security reasons
|
||||
<code>/config reload</code> - reloads the configuration file, i.e. after changes useful
|
||||
<code>/config set</code> KEY VALUE - sets a value for a certain key
|
||||
|
||||
<b>--- EXPLAINATION ---</b>
|
||||
Any user can submit/upload gore or any other type of content via dms (<tg-spoiler><i>t.me/{BOT_USERNAME}?start=submit</i></tg-spoiler>), it gets sent here. if you click Approve & send group, the link to the media gets generated and automatically sent to your public channel.
|
||||
|
||||
If you click approve, the link to the media gets generated and you can share it.
|
||||
|
||||
If you click deny, the submission gets deleted and no link is generated.
|
||||
|
||||
If you click deny & ban, the submission gets deleted, the user gets banned from using the bot again & also banned from all channels.
|
||||
|
||||
<tg-spoiler><u>Bot programmed by @{DEVELOPER_TELEGRAM_USERNAME}.</u></tg-spoiler>"""
|
||||
|
||||
|
||||
WARNING_MESSAGE = f"""The topic of this Bot is <code><b>{MEDIA_TOPIC_FOR_BOT.upper()}</b></code>. <b>Only submit media regarding that topic.</b>
|
||||
|
||||
<blockquote>
|
||||
{FORBIDDEN_CONTENT_TYPES_STRING} is <b>strictly forbidden</b> and leads to <b>permanent deletion of links</b> as well as <b>exclusion from bot</b>.
|
||||
</blockquote>
|
||||
|
||||
<code>Enter content ID</code> -> enter your content ID/link and the bot will upload all the videos/photos/texts to you in this chat, exposing the redirected hidden content
|
||||
|
||||
&
|
||||
|
||||
<code>Create Link To Media</code> -> upload your own content and generate link to it
|
||||
|
||||
&
|
||||
|
||||
<code>Submit Media to Admins</code> -> upload your own content and have admins review it, they will take action depending on what and how you submitted.
|
||||
|
||||
&
|
||||
|
||||
<code>Create Link To Text</code> -> Asks you to share contents (max. <code>{MAXIMUM_CHAR_LIMIT_FOR_TEXT_MSGS}</code> chars), then asks after how many views/usages your link shall get burned/data deleted.
|
||||
|
||||
&
|
||||
|
||||
<code>My links</code> -> shows all links you have created & their management options
|
||||
|
||||
|
||||
<b><u>Do you understand that by clicking proceed, you will see multiple follow up options, and possibly engage with extreme content?</u></b>"""
|
||||
|
||||
START_MESSAGE = f"""Hey, this bot is used to upload, store and create links to all sorts of cruel media. Please choose from options below.
|
||||
|
||||
<b><u>The prior message explains what the below options do pretty well.</u></b>
|
||||
|
||||
For contacting administrators, please use t.me/{CONTACT_TELEGERAM_LINK}."""
|
||||
0
resources/password_access_list.txt
Normal file
0
resources/password_access_list.txt
Normal file
8
roadmap.txt
Normal file
8
roadmap.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
1. Init the basic log
|
||||
2. Init all the databases if not existing, add schemes, tables ect.
|
||||
3. Init/online web server if used by users/admins
|
||||
4. Start the telegram bot
|
||||
|
||||
Signoff/web credits
|
||||
"Productions by t.me/forgecadrape
|
||||
Business inquiries 056834db96cedde6012da9d5402c683c0eb260ed866da145a8f86e7f329bf77222 on Session"
|
||||
0
web_files/files.js
Normal file
0
web_files/files.js
Normal file
286
web_files/index.html
Normal file
286
web_files/index.html
Normal file
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CANNIBAL GIRLS // ARCHIVE</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=VT323&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- CRT SCANLINE OVERLAY -->
|
||||
<div class="scanlines"></div>
|
||||
|
||||
<!-- NOISE TEXTURE OVERLAY -->
|
||||
<div class="noise-overlay"></div>
|
||||
|
||||
<!-- VIGNETTE -->
|
||||
<div class="vignette"></div>
|
||||
|
||||
<!-- LOADING SCREEN -->
|
||||
<section id="loading-screen" class="loading-screen">
|
||||
<div class="loading-content">
|
||||
<div class="loading-header">
|
||||
<div class="status-line">
|
||||
<span class="status-indicator"></span>
|
||||
<span class="status-text">SYSTEM ONLINE</span>
|
||||
</div>
|
||||
<div class="datetime" id="loading-datetime"></div>
|
||||
</div>
|
||||
|
||||
<div class="loading-center">
|
||||
<h1 class="loading-title" id="loading-title">CANNIBAL GIRLS</h1>
|
||||
<div class="loading-separator">
|
||||
<span class="sep-line"></span>
|
||||
<span class="sep-diamond">▼</span>
|
||||
<span class="sep-line"></span>
|
||||
</div>
|
||||
<p class="loading-subtitle" id="loading-subtitle">ARCHIVE TERMINAL v2.4.1</p>
|
||||
|
||||
<button id="load-files-btn" class="load-btn">
|
||||
<span class="btn-bracket">[</span>
|
||||
<span class="btn-text">LOAD FILES</span>
|
||||
<span class="btn-bracket">]</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="loading-footer">
|
||||
<div class="footer-top">
|
||||
<span class="footer-label">PRODUCER:</span>
|
||||
<span class="footer-value">t.me/forgecadrape</span>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span class="footer-label">SESSION:</span>
|
||||
<span class="footer-value session-id">056834db96cedde6012da9d5402c683c0eb260ed866da145a8f86e7f329bf77222</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- LOADING PHASE OVERLAY -->
|
||||
<section id="loading-phase" class="loading-phase hidden">
|
||||
<div class="phase-content">
|
||||
<div class="phase-header">
|
||||
<span class="phase-icon">▶</span>
|
||||
<span class="phase-title">INITIALIZING ARCHIVE PROTOCOL</span>
|
||||
</div>
|
||||
<div class="phase-terminal">
|
||||
<div class="terminal-output" id="terminal-output"></div>
|
||||
<div class="terminal-cursor">_</div>
|
||||
</div>
|
||||
<div class="phase-progress">
|
||||
<div class="progress-label">
|
||||
<span id="progress-text">MOUNTING VOLUMES...</span>
|
||||
<span id="progress-percent">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ARCHIVE BROWSER -->
|
||||
<main id="archive-browser" class="archive-browser hidden">
|
||||
<!-- TOP BAR -->
|
||||
<header class="archive-header">
|
||||
<div class="header-left">
|
||||
<h1 class="archive-title">CANNIBAL_GIRLS</h1>
|
||||
<span class="header-separator">//</span>
|
||||
<span class="header-subtitle">ARCHIVE_BROWSER</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-stats">
|
||||
<span class="stat-item">
|
||||
<span class="stat-label">FILES:</span>
|
||||
<span class="stat-value" id="file-count">0</span>
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<span class="stat-label">SIZE:</span>
|
||||
<span class="stat-value" id="total-size">0 GB</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-time" id="header-time"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MAIN CONTENT AREA -->
|
||||
<div class="archive-body">
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="archive-sidebar">
|
||||
<div class="sidebar-section">
|
||||
<h3 class="sidebar-title">CATALOG</h3>
|
||||
<ul class="sidebar-menu" id="sidebar-menu">
|
||||
<li class="sidebar-item active" data-filter="all">
|
||||
<span class="item-icon">■</span>
|
||||
<span class="item-label">ALL_FILES</span>
|
||||
<span class="item-count" id="count-all">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="video">
|
||||
<span class="item-icon">▶</span>
|
||||
<span class="item-label">VIDEO</span>
|
||||
<span class="item-count" id="count-video">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="image">
|
||||
<span class="item-icon">■</span>
|
||||
<span class="item-label">IMAGES</span>
|
||||
<span class="item-count" id="count-image">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="audio">
|
||||
<span class="item-icon">♬</span>
|
||||
<span class="item-label">AUDIO</span>
|
||||
<span class="item-count" id="count-audio">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="document">
|
||||
<span class="item-icon">☰</span>
|
||||
<span class="item-label">DOCS</span>
|
||||
<span class="item-count" id="count-document">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="archive">
|
||||
<span class="item-icon">■</span>
|
||||
<span class="item-label">ARCHIVES</span>
|
||||
<span class="item-count" id="count-archive">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3 class="sidebar-title">SYSTEM</h3>
|
||||
<div class="system-info">
|
||||
<div class="sys-row">
|
||||
<span class="sys-label">STATUS:</span>
|
||||
<span class="sys-value status-ok">ONLINE</span>
|
||||
</div>
|
||||
<div class="sys-row">
|
||||
<span class="sys-label">UPTIME:</span>
|
||||
<span class="sys-value" id="uptime">00:00:00</span>
|
||||
</div>
|
||||
<div class="sys-row">
|
||||
<span class="sys-label">VERSION:</span>
|
||||
<span class="sys-value">2.4.1-b</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- FILE LIST AREA -->
|
||||
<div class="archive-content">
|
||||
<!-- TOOLBAR -->
|
||||
<div class="content-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<div class="view-switcher">
|
||||
<button class="view-btn active" data-view="list" title="List View">
|
||||
<span class="view-icon">☰</span>
|
||||
<span class="view-label">LIST</span>
|
||||
</button>
|
||||
<button class="view-btn" data-view="grid" title="Grid View">
|
||||
<span class="view-icon">■■</span>
|
||||
<span class="view-label">GRID</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sort-controls">
|
||||
<span class="sort-label">SORT:</span>
|
||||
<select id="sort-select" class="sort-select">
|
||||
<option value="name-asc">NAME [A-Z]</option>
|
||||
<option value="name-desc">NAME [Z-A]</option>
|
||||
<option value="date-desc">DATE [NEWEST]</option>
|
||||
<option value="date-asc">DATE [OLDEST]</option>
|
||||
<option value="size-desc">SIZE [LARGEST]</option>
|
||||
<option value="size-asc">SIZE [SMALLEST]</option>
|
||||
<option value="type">TYPE</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<div class="search-box">
|
||||
<span class="search-icon">⚲</span>
|
||||
<input type="text" id="search-input" class="search-input" placeholder="SEARCH FILES...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILE LIST -->
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header" id="file-list-header">
|
||||
<span class="col-name">NAME</span>
|
||||
<span class="col-type">TYPE</span>
|
||||
<span class="col-size">SIZE</span>
|
||||
<span class="col-date">DATE</span>
|
||||
</div>
|
||||
<div class="file-list" id="file-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PREVIEW PANE -->
|
||||
<aside class="archive-preview" id="preview-pane">
|
||||
<div class="preview-header">
|
||||
<span class="preview-title">PREVIEW</span>
|
||||
<button class="preview-close" id="preview-close">✕</button>
|
||||
</div>
|
||||
<div class="preview-content" id="preview-content">
|
||||
<div class="preview-placeholder">
|
||||
<span class="placeholder-icon">■</span>
|
||||
<span class="placeholder-text">SELECT A FILE TO PREVIEW</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-meta" id="preview-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">FILENAME:</span>
|
||||
<span class="meta-value" id="meta-filename">---</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">TYPE:</span>
|
||||
<span class="meta-value" id="meta-type">---</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">SIZE:</span>
|
||||
<span class="meta-value" id="meta-size">---</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">CREATED:</span>
|
||||
<span class="meta-value" id="meta-created">---</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">MODIFIED:</span>
|
||||
<span class="meta-value" id="meta-modified">---</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">CHECKSUM:</span>
|
||||
<span class="meta-value checksum" id="meta-checksum">---</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<button class="action-btn" id="action-download">
|
||||
<span class="action-icon">▼</span>
|
||||
DOWNLOAD
|
||||
</button>
|
||||
<button class="action-btn" id="action-copy">
|
||||
<span class="action-icon">⚬</span>
|
||||
COPY HASH
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="archive-footer">
|
||||
<div class="footer-left">
|
||||
<span class="footer-item">PROD: t.me/forgecadrape</span>
|
||||
</div>
|
||||
<div class="footer-center">
|
||||
<span class="footer-item" id="footer-status">READY</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="footer-item session-truncated">SESSION: 056834db96ce...f77222</span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- AUDIO ELEMENT -->
|
||||
<audio id="bg-audio" preload="auto"></audio>
|
||||
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
666
web_files/index.js
Normal file
666
web_files/index.js
Normal file
@@ -0,0 +1,666 @@
|
||||
/**
|
||||
* CANNIBAL GIRLS - ARCHIVE TERMINAL
|
||||
* Main Application Logic
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ========================================
|
||||
// CONFIGURATION
|
||||
// ========================================
|
||||
const CONFIG = {
|
||||
loadDelayMin: 4000,
|
||||
loadDelayMax: 9000,
|
||||
audioVolume: 0.15,
|
||||
audioPath: 'audio/',
|
||||
terminalLines: [
|
||||
{ text: 'Mounting storage volumes...', type: 'normal', delay: 0 },
|
||||
{ text: 'Checking filesystem integrity...', type: 'normal', delay: 300 },
|
||||
{ text: 'Scanning /archive/repository...', type: 'normal', delay: 600 },
|
||||
{ text: 'Found 247 indexed files', type: 'success', delay: 1200 },
|
||||
{ text: 'Verifying checksums...', type: 'normal', delay: 1500 },
|
||||
{ text: 'Warning: 3 files flagged for review', type: 'warning', delay: 2200 },
|
||||
{ text: 'Rebuilding file index...', type: 'normal', delay: 2500 },
|
||||
{ text: 'Optimizing cache...', type: 'normal', delay: 3200 },
|
||||
{ text: 'Archive mount successful', type: 'success', delay: 4000 },
|
||||
{ text: 'Ready for access', type: 'success', delay: 4500 }
|
||||
]
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// DEMO FILE DATA
|
||||
// ========================================
|
||||
const DEMO_FILES = [
|
||||
{ id: 1, name: 'project_venus_raw.mp4', type: 'video', size: 2847563210, date: '2024-01-15T14:23:00', checksum: 'a3f7c9d2e1b4f5a6c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1' },
|
||||
{ id: 2, name: 'archive_segment_07.mkv', type: 'video', size: 1456789012, date: '2024-01-14T09:15:00', checksum: 'b4c8d0e3f2a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c2' },
|
||||
{ id: 3, name: 'nocturne_stills_set.zip', type: 'archive', size: 456782345, date: '2024-01-13T16:45:00', checksum: 'c5d9e1f4a3b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d3' },
|
||||
{ id: 4, name: 'soundscape_01.flac', type: 'audio', size: 124567890, date: '2024-01-12T11:30:00', checksum: 'd6e0f2a5b4c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e4' },
|
||||
{ id: 5, name: 'behind_the_scenes_03.jpg', type: 'image', size: 8945678, date: '2024-01-11T08:20:00', checksum: 'e7f1a3b6c5d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f5' },
|
||||
{ id: 6, name: 'production_notes_v2.pdf', type: 'document', size: 2345678, date: '2024-01-10T19:00:00', checksum: 'f8a2b4c7d6e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a6' },
|
||||
{ id: 7, name: 'raw_footage_reel_12.mp4', type: 'video', size: 5678901234, date: '2024-01-09T13:10:00', checksum: 'a9b3c5d7e8f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7' },
|
||||
{ id: 8, name: 'color_grade_lut.cube', type: 'document', size: 45678, date: '2024-01-08T10:45:00', checksum: 'b0c4d6e8f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8' },
|
||||
{ id: 9, name: 'ambient_collection.zip', type: 'archive', size: 345678901, date: '2024-01-07T15:30:00', checksum: 'c1d5e7f9a0b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9' },
|
||||
{ id: 10, name: 'title_sequence_v3.mov', type: 'video', size: 1234567890, date: '2024-01-06T07:00:00', checksum: 'd2e6f8a0b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0' },
|
||||
{ id: 11, name: 'location_scout_photos.zip', type: 'archive', size: 156789012, date: '2024-01-05T12:15:00', checksum: 'e3f7a9b1c2d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1' },
|
||||
{ id: 12, name: 'score_master_24bit.wav', type: 'audio', size: 890123456, date: '2024-01-04T18:30:00', checksum: 'f4a8b0c2d3e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2' },
|
||||
{ id: 13, name: 'character_studies_01.png', type: 'image', size: 45678901, date: '2024-01-03T09:45:00', checksum: 'a5b9c1d3e4f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3' },
|
||||
{ id: 14, name: 'edit_timeline_v4.prproj', type: 'document', size: 12345678, date: '2024-01-02T14:00:00', checksum: 'b6c0d2e4f5a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4' },
|
||||
{ id: 15, name: 'promo_cut_30sec.mp4', type: 'video', size: 678901234, date: '2024-01-01T11:20:00', checksum: 'c7d1e3f5a6b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5' }
|
||||
];
|
||||
|
||||
// ========================================
|
||||
// STATE
|
||||
// ========================================
|
||||
let state = {
|
||||
files: [],
|
||||
filteredFiles: [],
|
||||
selectedFile: null,
|
||||
currentView: 'list',
|
||||
currentFilter: 'all',
|
||||
currentSort: 'date-desc',
|
||||
searchQuery: '',
|
||||
uptime: 0,
|
||||
audioContext: null
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// DOM REFERENCES
|
||||
// ========================================
|
||||
const els = {};
|
||||
|
||||
function cacheElements() {
|
||||
els.loadingScreen = document.getElementById('loading-screen');
|
||||
els.loadingTitle = document.getElementById('loading-title');
|
||||
els.loadingDatetime = document.getElementById('loading-datetime');
|
||||
els.loadBtn = document.getElementById('load-files-btn');
|
||||
els.loadingPhase = document.getElementById('loading-phase');
|
||||
els.terminalOutput = document.getElementById('terminal-output');
|
||||
els.progressText = document.getElementById('progress-text');
|
||||
els.progressPercent = document.getElementById('progress-percent');
|
||||
els.progressFill = document.getElementById('progress-fill');
|
||||
els.archiveBrowser = document.getElementById('archive-browser');
|
||||
els.fileList = document.getElementById('file-list');
|
||||
els.fileListHeader = document.getElementById('file-list-header');
|
||||
els.sidebarMenu = document.getElementById('sidebar-menu');
|
||||
els.sortSelect = document.getElementById('sort-select');
|
||||
els.searchInput = document.getElementById('search-input');
|
||||
els.previewPane = document.getElementById('preview-pane');
|
||||
els.previewContent = document.getElementById('preview-content');
|
||||
els.previewClose = document.getElementById('preview-close');
|
||||
els.previewMeta = document.getElementById('preview-meta');
|
||||
els.headerTime = document.getElementById('header-time');
|
||||
els.uptime = document.getElementById('uptime');
|
||||
els.fileCount = document.getElementById('file-count');
|
||||
els.totalSize = document.getElementById('total-size');
|
||||
els.footerStatus = document.getElementById('footer-status');
|
||||
els.bgAudio = document.getElementById('bg-audio');
|
||||
els.actionDownload = document.getElementById('action-download');
|
||||
els.actionCopy = document.getElementById('action-copy');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UTILITIES
|
||||
// ========================================
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
function formatDateShort(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getFileIcon(type) {
|
||||
const icons = {
|
||||
video: '▶',
|
||||
audio: '♬',
|
||||
image: '■',
|
||||
document: '☰',
|
||||
archive: '■'
|
||||
};
|
||||
return icons[type] || '■';
|
||||
}
|
||||
|
||||
function getRandomDelay() {
|
||||
return Math.floor(Math.random() * (CONFIG.loadDelayMax - CONFIG.loadDelayMin + 1)) + CONFIG.loadDelayMin;
|
||||
}
|
||||
|
||||
function generateChecksum() {
|
||||
const chars = 'abcdef0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 64; i++) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TEXT SCRAMBLE EFFECT
|
||||
// ========================================
|
||||
class TextScramble {
|
||||
constructor(el) {
|
||||
this.el = el;
|
||||
this.chars = '!<>-_\\/[]{}--=+*^?#________';
|
||||
this.update = this.update.bind(this);
|
||||
}
|
||||
|
||||
setText(newText) {
|
||||
const oldText = this.el.innerText;
|
||||
const length = Math.max(oldText.length, newText.length);
|
||||
const promise = new Promise(resolve => this.resolve = resolve);
|
||||
this.queue = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
const from = oldText[i] || '';
|
||||
const to = newText[i] || '';
|
||||
const start = Math.floor(Math.random() * 40);
|
||||
const end = start + Math.floor(Math.random() * 40);
|
||||
this.queue.push({ from, to, start, end });
|
||||
}
|
||||
cancelAnimationFrame(this.frameRequest);
|
||||
this.frame = 0;
|
||||
this.update();
|
||||
return promise;
|
||||
}
|
||||
|
||||
update() {
|
||||
let output = '';
|
||||
let complete = 0;
|
||||
for (let i = 0, n = this.queue.length; i < n; i++) {
|
||||
let { from, to, start, end, char } = this.queue[i];
|
||||
if (this.frame >= end) {
|
||||
complete++;
|
||||
output += to;
|
||||
} else if (this.frame >= start) {
|
||||
if (!char || Math.random() < 0.28) {
|
||||
char = this.randomChar();
|
||||
this.queue[i].char = char;
|
||||
}
|
||||
output += `<span class="text-dim">${char}</span>`;
|
||||
} else {
|
||||
output += from;
|
||||
}
|
||||
}
|
||||
this.el.innerHTML = output;
|
||||
if (complete === this.queue.length) {
|
||||
this.resolve();
|
||||
} else {
|
||||
this.frameRequest = requestAnimationFrame(this.update);
|
||||
this.frame++;
|
||||
}
|
||||
}
|
||||
|
||||
randomChar() {
|
||||
return this.chars[Math.floor(Math.random() * this.chars.length)];
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// AUDIO MANAGEMENT
|
||||
// ========================================
|
||||
async function getAudioFiles() {
|
||||
// In a real scenario, this would fetch from the server
|
||||
// For now, we'll try common audio filenames or return empty
|
||||
const commonNames = ['ambient.mp3', 'noise.mp3', 'static.mp3', 'drone.mp3', 'hum.mp3'];
|
||||
return commonNames.map(name => CONFIG.audioPath + name);
|
||||
}
|
||||
|
||||
async function playRandomAudio() {
|
||||
try {
|
||||
const audioFiles = await getAudioFiles();
|
||||
if (audioFiles.length === 0) {
|
||||
console.log('No audio files found in', CONFIG.audioPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const randomFile = audioFiles[Math.floor(Math.random() * audioFiles.length)];
|
||||
els.bgAudio.src = randomFile;
|
||||
els.bgAudio.volume = CONFIG.audioVolume;
|
||||
|
||||
const playPromise = els.bgAudio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch(err => {
|
||||
console.log('Audio playback failed:', err.message);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Audio error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function stopAudio() {
|
||||
els.bgAudio.pause();
|
||||
els.bgAudio.currentTime = 0;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TERMINAL OUTPUT
|
||||
// ========================================
|
||||
function addTerminalLine(text, type = 'normal') {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'terminal-line';
|
||||
|
||||
const prompt = document.createElement('span');
|
||||
prompt.className = 'line-prompt';
|
||||
prompt.textContent = '>';
|
||||
|
||||
const content = document.createElement('span');
|
||||
content.className = `line-content ${type}`;
|
||||
content.textContent = text;
|
||||
|
||||
line.appendChild(prompt);
|
||||
line.appendChild(content);
|
||||
els.terminalOutput.appendChild(line);
|
||||
|
||||
// Auto-scroll
|
||||
els.terminalOutput.scrollTop = els.terminalOutput.scrollHeight;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LOADING SEQUENCE
|
||||
// ========================================
|
||||
async function startLoadingSequence() {
|
||||
const delay = getRandomDelay();
|
||||
|
||||
// Hide loading screen, show loading phase
|
||||
els.loadBtn.disabled = true;
|
||||
els.loadBtn.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
setTimeout(() => {
|
||||
els.loadingScreen.classList.add('hidden');
|
||||
els.loadingPhase.classList.remove('hidden');
|
||||
|
||||
// Start audio
|
||||
playRandomAudio();
|
||||
|
||||
// Start terminal output
|
||||
let currentTime = 0;
|
||||
CONFIG.terminalLines.forEach(line => {
|
||||
setTimeout(() => {
|
||||
addTerminalLine(line.text, line.type);
|
||||
}, line.delay);
|
||||
});
|
||||
|
||||
// Progress bar
|
||||
const startTime = Date.now();
|
||||
const updateProgress = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min((elapsed / delay) * 100, 100);
|
||||
|
||||
els.progressFill.style.width = progress + '%';
|
||||
els.progressPercent.textContent = Math.floor(progress) + '%';
|
||||
|
||||
// Update progress text based on percentage
|
||||
if (progress < 20) els.progressText.textContent = 'MOUNTING VOLUMES...';
|
||||
else if (progress < 40) els.progressText.textContent = 'VERIFYING CHECKSUMS...';
|
||||
else if (progress < 60) els.progressText.textContent = 'INDEXING FILES...';
|
||||
else if (progress < 80) els.progressText.textContent = 'OPTIMIZING CACHE...';
|
||||
else if (progress < 100) els.progressText.textContent = 'FINALIZING...';
|
||||
else els.progressText.textContent = 'COMPLETE';
|
||||
|
||||
if (progress < 100) {
|
||||
requestAnimationFrame(updateProgress);
|
||||
} else {
|
||||
setTimeout(finishLoading, 500);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(updateProgress);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function finishLoading() {
|
||||
stopAudio();
|
||||
els.loadingPhase.classList.add('hidden');
|
||||
els.archiveBrowser.classList.remove('hidden');
|
||||
els.archiveBrowser.classList.add('active');
|
||||
|
||||
// Initialize archive
|
||||
initArchive();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ARCHIVE INITIALIZATION
|
||||
// ========================================
|
||||
function initArchive() {
|
||||
state.files = DEMO_FILES.map(f => ({
|
||||
...f,
|
||||
checksum: f.checksum || generateChecksum()
|
||||
}));
|
||||
|
||||
updateCounts();
|
||||
filterAndSortFiles();
|
||||
renderFiles();
|
||||
updateStats();
|
||||
|
||||
// Start uptime counter
|
||||
setInterval(() => {
|
||||
state.uptime++;
|
||||
updateUptime();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
const counts = {
|
||||
all: state.files.length,
|
||||
video: state.files.filter(f => f.type === 'video').length,
|
||||
image: state.files.filter(f => f.type === 'image').length,
|
||||
audio: state.files.filter(f => f.type === 'audio').length,
|
||||
document: state.files.filter(f => f.type === 'document').length,
|
||||
archive: state.files.filter(f => f.type === 'archive').length
|
||||
};
|
||||
|
||||
Object.keys(counts).forEach(key => {
|
||||
const el = document.getElementById(`count-${key}`);
|
||||
if (el) el.textContent = counts[key];
|
||||
});
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
els.fileCount.textContent = state.filteredFiles.length;
|
||||
const totalBytes = state.filteredFiles.reduce((sum, f) => sum + f.size, 0);
|
||||
els.totalSize.textContent = formatBytes(totalBytes);
|
||||
}
|
||||
|
||||
function updateUptime() {
|
||||
const hours = Math.floor(state.uptime / 3600).toString().padStart(2, '0');
|
||||
const minutes = Math.floor((state.uptime % 3600) / 60).toString().padStart(2, '0');
|
||||
const seconds = (state.uptime % 60).toString().padStart(2, '0');
|
||||
els.uptime.textContent = `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FILE FILTERING & SORTING
|
||||
// ========================================
|
||||
function filterAndSortFiles() {
|
||||
let files = [...state.files];
|
||||
|
||||
// Apply filter
|
||||
if (state.currentFilter !== 'all') {
|
||||
files = files.filter(f => f.type === state.currentFilter);
|
||||
}
|
||||
|
||||
// Apply search
|
||||
if (state.searchQuery) {
|
||||
const query = state.searchQuery.toLowerCase();
|
||||
files = files.filter(f => f.name.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
// Apply sort
|
||||
files.sort((a, b) => {
|
||||
switch (state.currentSort) {
|
||||
case 'name-asc':
|
||||
return a.name.localeCompare(b.name);
|
||||
case 'name-desc':
|
||||
return b.name.localeCompare(a.name);
|
||||
case 'date-desc':
|
||||
return new Date(b.date) - new Date(a.date);
|
||||
case 'date-asc':
|
||||
return new Date(a.date) - new Date(b.date);
|
||||
case 'size-desc':
|
||||
return b.size - a.size;
|
||||
case 'size-asc':
|
||||
return a.size - b.size;
|
||||
case 'type':
|
||||
return a.type.localeCompare(b.type) || a.name.localeCompare(b.name);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
state.filteredFiles = files;
|
||||
updateStats();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FILE RENDERING
|
||||
// ========================================
|
||||
function renderFiles() {
|
||||
els.fileList.innerHTML = '';
|
||||
|
||||
if (state.filteredFiles.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'file-item';
|
||||
empty.innerHTML = '<span style="color: #5A4545; grid-column: 1 / -1; text-align: center; padding: 2rem;">NO FILES FOUND</span>';
|
||||
els.fileList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
state.filteredFiles.forEach((file, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'file-item';
|
||||
if (state.selectedFile && state.selectedFile.id === file.id) {
|
||||
item.classList.add('selected');
|
||||
}
|
||||
|
||||
// Add staggered animation
|
||||
item.style.animation = `fadeInUp 0.3s ease-out ${index * 0.03}s both`;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="file-name-cell">
|
||||
<span class="file-icon">${getFileIcon(file.type)}</span>
|
||||
<span class="file-name" title="${file.name}">${file.name}</span>
|
||||
</div>
|
||||
<span class="file-type">${file.type}</span>
|
||||
<span class="file-size">${formatBytes(file.size)}</span>
|
||||
<span class="file-date">${formatDateShort(file.date)}</span>
|
||||
`;
|
||||
|
||||
item.addEventListener('click', () => selectFile(file));
|
||||
els.fileList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FILE SELECTION & PREVIEW
|
||||
// ========================================
|
||||
function selectFile(file) {
|
||||
state.selectedFile = file;
|
||||
|
||||
// Update selection in list
|
||||
document.querySelectorAll('.file-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
const selectedEl = Array.from(els.fileList.children).find(el =>
|
||||
el.querySelector('.file-name')?.textContent === file.name
|
||||
);
|
||||
if (selectedEl) selectedEl.classList.add('selected');
|
||||
|
||||
// Update preview
|
||||
updatePreview(file);
|
||||
|
||||
// Update footer status
|
||||
els.footerStatus.textContent = `SELECTED: ${file.name}`;
|
||||
}
|
||||
|
||||
function updatePreview(file) {
|
||||
// Update preview content
|
||||
els.previewContent.innerHTML = `
|
||||
<div class="preview-placeholder">
|
||||
<span class="preview-file-icon">${getFileIcon(file.type)}</span>
|
||||
<span class="placeholder-text">${file.type.toUpperCase()} FILE</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update meta
|
||||
document.getElementById('meta-filename').textContent = file.name;
|
||||
document.getElementById('meta-type').textContent = file.type.toUpperCase();
|
||||
document.getElementById('meta-size').textContent = formatBytes(file.size);
|
||||
document.getElementById('meta-created').textContent = formatDate(file.date);
|
||||
document.getElementById('meta-modified').textContent = formatDate(file.date);
|
||||
document.getElementById('meta-checksum').textContent = file.checksum;
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
state.selectedFile = null;
|
||||
document.querySelectorAll('.file-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
els.previewContent.innerHTML = `
|
||||
<div class="preview-placeholder">
|
||||
<span class="placeholder-icon">■</span>
|
||||
<span class="placeholder-text">SELECT A FILE TO PREVIEW</span>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('meta-filename').textContent = '---';
|
||||
document.getElementById('meta-type').textContent = '---';
|
||||
document.getElementById('meta-size').textContent = '---';
|
||||
document.getElementById('meta-created').textContent = '---';
|
||||
document.getElementById('meta-modified').textContent = '---';
|
||||
document.getElementById('meta-checksum').textContent = '---';
|
||||
els.footerStatus.textContent = 'READY';
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VIEW SWITCHING
|
||||
// ========================================
|
||||
function switchView(view) {
|
||||
state.currentView = view;
|
||||
|
||||
// Update buttons
|
||||
document.querySelectorAll('.view-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.view === view);
|
||||
});
|
||||
|
||||
// Update list class
|
||||
if (view === 'grid') {
|
||||
els.fileList.classList.add('grid-view');
|
||||
els.fileListHeader.style.display = 'none';
|
||||
} else {
|
||||
els.fileList.classList.remove('grid-view');
|
||||
els.fileListHeader.style.display = 'grid';
|
||||
}
|
||||
|
||||
renderFiles();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EVENT LISTENERS
|
||||
// ========================================
|
||||
function bindEvents() {
|
||||
// Load button
|
||||
els.loadBtn.addEventListener('click', startLoadingSequence);
|
||||
|
||||
// Sidebar filters
|
||||
els.sidebarMenu.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.sidebar-item');
|
||||
if (!item) return;
|
||||
|
||||
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
state.currentFilter = item.dataset.filter;
|
||||
filterAndSortFiles();
|
||||
renderFiles();
|
||||
});
|
||||
|
||||
// View switcher
|
||||
document.querySelectorAll('.view-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
||||
});
|
||||
|
||||
// Sort
|
||||
els.sortSelect.addEventListener('change', (e) => {
|
||||
state.currentSort = e.target.value;
|
||||
filterAndSortFiles();
|
||||
renderFiles();
|
||||
});
|
||||
|
||||
// Search
|
||||
els.searchInput.addEventListener('input', (e) => {
|
||||
state.searchQuery = e.target.value;
|
||||
filterAndSortFiles();
|
||||
renderFiles();
|
||||
});
|
||||
|
||||
// Preview close
|
||||
els.previewClose.addEventListener('click', closePreview);
|
||||
|
||||
// Download button
|
||||
els.actionDownload.addEventListener('click', () => {
|
||||
if (!state.selectedFile) return;
|
||||
|
||||
const blob = new Blob([`File: ${state.selectedFile.name}\nSize: ${formatBytes(state.selectedFile.size)}\nChecksum: ${state.selectedFile.checksum}`], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = state.selectedFile.name + '.info.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
els.footerStatus.textContent = 'DOWNLOADED INFO FILE';
|
||||
setTimeout(() => {
|
||||
els.footerStatus.textContent = 'READY';
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Copy hash button
|
||||
els.actionCopy.addEventListener('click', () => {
|
||||
if (!state.selectedFile) return;
|
||||
|
||||
navigator.clipboard.writeText(state.selectedFile.checksum).then(() => {
|
||||
els.footerStatus.textContent = 'CHECKSUM COPIED TO CLIPBOARD';
|
||||
setTimeout(() => {
|
||||
els.footerStatus.textContent = 'READY';
|
||||
}, 2000);
|
||||
}).catch(() => {
|
||||
els.footerStatus.textContent = 'COPY FAILED';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CLOCK UPDATES
|
||||
// ========================================
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
if (els.loadingDatetime) {
|
||||
els.loadingDatetime.textContent = dateStr;
|
||||
}
|
||||
if (els.headerTime) {
|
||||
els.headerTime.textContent = dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INITIALIZATION
|
||||
// ========================================
|
||||
function init() {
|
||||
cacheElements();
|
||||
|
||||
// Title scramble effect
|
||||
const scrambler = new TextScramble(els.loadingTitle);
|
||||
setTimeout(() => {
|
||||
scrambler.setText('CANNIBAL GIRLS');
|
||||
}, 500);
|
||||
|
||||
// Clock
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
// Bind events
|
||||
bindEvents();
|
||||
|
||||
console.log('%c CANNIBAL GIRLS // ARCHIVE TERMINAL v2.4.1 ', 'background: #0F0D0D; color: #E85D75; font-family: monospace; padding: 4px 8px;');
|
||||
console.log('%c SYSTEM ONLINE ', 'background: #181414; color: #4A7C59; font-family: monospace; padding: 2px 6px;');
|
||||
}
|
||||
|
||||
// Start when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
0
web_files/resources.js
Normal file
0
web_files/resources.js
Normal file
1386
web_files/style.css
Normal file
1386
web_files/style.css
Normal file
File diff suppressed because it is too large
Load Diff
0
webhandler.py
Normal file
0
webhandler.py
Normal file
0
webloader.py
Normal file
0
webloader.py
Normal file
Reference in New Issue
Block a user