Compare commits
21 commits
Author | SHA1 | Date | |
---|---|---|---|
da9b879116 | |||
5535ffd048 | |||
09c0f12692 | |||
5be65229f4 | |||
ebb7edcc8b | |||
831f3a5f98 | |||
ac97e5b152 | |||
faf221c9ba | |||
c95aed01b5 | |||
59d092c9d0 | |||
23a00d2c06 | |||
da077519fe | |||
4921d4bd6f | |||
d786d7f7af | |||
b62f6d87c7 | |||
e5932844e2 | |||
4cddc90172 | |||
70d53ff8a0 | |||
55ac4b7502 | |||
0dee6d64ed | |||
410cba70e5 |
50 changed files with 3058 additions and 935 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,2 +1,7 @@
|
|||
.venv/
|
||||
venv/
|
||||
__pycache__/
|
||||
.idea/
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
*.db
|
||||
store/
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Ситуация
|
||||
|
||||
На свелте мне нравится что получается что-то, но пока не нравится что получается.
|
||||
Как будто бы надо будет сделать больбшой рефакторинг, но пока пускай будет хоть что-то.
|
70
backend/api.py
Normal file
70
backend/api.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from typing import Annotated
|
||||
from fastapi import FastAPI, File, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from vntypes import *
|
||||
from utils import *
|
||||
from db import VNDB
|
||||
|
||||
app = FastAPI()
|
||||
database = VNDB()
|
||||
|
||||
origins = [
|
||||
"http://localhost",
|
||||
"http://localhost:5173",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/api/queue")
|
||||
def get_post_queue() -> list[FullNovel]:
|
||||
raise HTTPException(status_code=501, detail="Method is not implemented yet!")
|
||||
|
||||
|
||||
@app.get("/api/posts/{post_id}")
|
||||
def get_post(post_id: int):
|
||||
raise HTTPException(status_code=501, detail="Method is not implemented yet!")
|
||||
|
||||
|
||||
@app.post("/api/posts/{post_id}")
|
||||
def new_post(novel: FullNovel):
|
||||
print(novel)
|
||||
return "yay!"
|
||||
|
||||
|
||||
@app.patch("/api/posts/{post_id}")
|
||||
def edit_post(novel: FullNovel):
|
||||
print(novel)
|
||||
return "yay!"
|
||||
|
||||
|
||||
@app.post("/api/mark")
|
||||
def new_mark(mark: Mark):
|
||||
database.insert_mark(mark.type, mark.value)
|
||||
return "yay!"
|
||||
|
||||
|
||||
@app.post("/api/marks")
|
||||
def search_marks(query: Mark):
|
||||
return database.search_mark(query)
|
||||
|
||||
|
||||
@app.post("/api/thumb")
|
||||
async def upload_thumb(thumb: Annotated[bytes, File()], filename: str):
|
||||
return {"file_size": save_image(thumb, "thumbs", filename)}
|
||||
|
||||
|
||||
@app.post("/api/screenshot")
|
||||
async def upload_screenshot(scrshot: Annotated[bytes, File()], filename: str):
|
||||
return {"file_size": save_image(scrshot, "screens", filename)}
|
||||
|
||||
|
||||
@app.post("/api/file")
|
||||
async def upload_file(file: Annotated[bytes, File()], filename: str):
|
||||
return {"file_size": save_file(file, "files", filename)}
|
111
backend/db.py
Normal file
111
backend/db.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
from datetime import datetime
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
from utils import asset
|
||||
from vntypes import *
|
||||
|
||||
class VNDB:
|
||||
|
||||
def __init__(self, db_name=asset('vn_database.db')):
|
||||
self.__db_name = db_name
|
||||
with sqlite3.connect(self.__db_name) as connection:
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS marks (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
UNIQUE(id, value)
|
||||
);
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS authors (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
thumbnail TEXT NOT NULL,
|
||||
UNIQUE(id, title, description, thumbnail)
|
||||
);
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS novels (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
author INTEGER NOT NULL,
|
||||
UNIQUE(id, title, author)
|
||||
);
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS posts_log (
|
||||
novel INTEGER NOT NULL,
|
||||
channel INTEGER NOT NULL,
|
||||
marks JSON NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
author INTEGER NOT NULL,
|
||||
files_on_disk JSON NOT NULL,
|
||||
post_info JSON,
|
||||
post_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
''')
|
||||
|
||||
connection.commit()
|
||||
|
||||
def __execute(self, sql: str, params: tuple = ()):
|
||||
with sqlite3.connect(self.__db_name) as connection:
|
||||
connection.cursor().execute(sql, params)
|
||||
connection.commit()
|
||||
|
||||
def __fetch(self, sql: str, params: tuple = ()):
|
||||
with sqlite3.connect(self.__db_name) as connection:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(sql, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
def search_author(self, title: str):
|
||||
return self.__fetch("SELECT * FROM authors "
|
||||
"WHERE title LIKE ?", (title,))
|
||||
|
||||
def search_mark(self, mark: Mark):
|
||||
return self.__fetch("SELECT * FROM marks "
|
||||
"WHERE value LIKE ? "
|
||||
"AND type = ?", ('%'+mark.value+'%',mark.type))
|
||||
|
||||
def search_novel(self, title: str):
|
||||
return self.__fetch("SELECT * FROM novels "
|
||||
"WHERE title LIKE ? ", (title,))
|
||||
|
||||
def insert_author(self, title: str, desc: str, thumb: str):
|
||||
self.__execute("INSERT INTO authors (title, description, thumbnail) "
|
||||
"VALUES (?, ?, ?)", (title, desc, thumb))
|
||||
|
||||
def insert_mark(self, type: str, value: str):
|
||||
self.__execute("INSERT INTO marks (type, value) "
|
||||
"VALUES (?, ?)", (type, value))
|
||||
|
||||
def insert_novel(self, novel: Novel):
|
||||
self.__execute("INSERT INTO novels (title, author) "
|
||||
f"SELECT {novel.title}, {novel.author_id} "
|
||||
f"WHERE NOT EXISTS(SELECT 1 FROM novels WHERE title = {novel.title} AND author = {novel.author_id});")
|
||||
|
||||
# FIXME: SQL Types
|
||||
def insert_post(self, full_novel: FullNovel):
|
||||
self.insert_novel(full_novel.data)
|
||||
self.__execute("INSERT INTO posts_log (novel, channel, marks, title, description, author, files_on_disk, post_at, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(self.search_novel(full_novel.data.title)[0],
|
||||
full_novel.data.tg_channel,
|
||||
json.dumps(full_novel.data.marks),
|
||||
full_novel.data.title,
|
||||
full_novel.data.description,
|
||||
full_novel.data.author_id,
|
||||
json.dumps(full_novel.files),
|
||||
full_novel.data.post_at,
|
||||
datetime.now()))
|
||||
|
34
backend/utils.py
Normal file
34
backend/utils.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from PIL import Image, UnidentifiedImageError
|
||||
from fastapi import HTTPException
|
||||
from pathlib import Path
|
||||
import io
|
||||
|
||||
def asset(path: str) -> Path:
|
||||
return Path("store", path)
|
||||
|
||||
|
||||
def image_normalize(img: bytes) -> bytes:
|
||||
try:
|
||||
byte_arr = io.BytesIO()
|
||||
img = Image.open(io.BytesIO(img))
|
||||
img.save(byte_arr, format='WEBP')
|
||||
return byte_arr.getvalue()
|
||||
except UnidentifiedImageError:
|
||||
raise HTTPException(status_code=500, detail="Image file cannot be readed!")
|
||||
|
||||
|
||||
def save_image(img: bytes, dir: str, name: str) -> int:
|
||||
path = asset(dir)
|
||||
path.mkdir(exist_ok=True)
|
||||
img = image_normalize(img)
|
||||
with open(Path(path, name+'.jpg'), "wb") as file:
|
||||
file.write(img)
|
||||
return len(img)
|
||||
|
||||
|
||||
def save_file(file_b: bytes, dir: str, name: str) -> int:
|
||||
path = asset(dir)
|
||||
path.mkdir(exist_ok=True)
|
||||
with open(Path(path, name+'.jpg'), "wb") as file:
|
||||
file.write(file_b)
|
||||
return len(file_b)
|
33
backend/vntypes.py
Normal file
33
backend/vntypes.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
class Mark(BaseModel):
|
||||
type: Literal["tag", "badge", "genre"]
|
||||
value: str
|
||||
|
||||
class NovelFile(BaseModel):
|
||||
filename: str
|
||||
platform: Literal["android", "ios", "windows", "linux", "macos"]
|
||||
|
||||
class Novel(BaseModel):
|
||||
title: str
|
||||
description: str
|
||||
author_id: int
|
||||
|
||||
vndb: int | None = None
|
||||
hours_to_read: int
|
||||
|
||||
marks: list[Mark]
|
||||
|
||||
tg_channel: int # maybe not here
|
||||
tg_post: str | None = None #url::Url
|
||||
post_at: datetime | None = None
|
||||
|
||||
class FullNovel(BaseModel):
|
||||
data: Novel
|
||||
|
||||
#upload_queue: list[str]
|
||||
files: list[NovelFile]
|
||||
screenshots: list[str]
|
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
38
frontend/README.md
Normal file
38
frontend/README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
19
frontend/jsconfig.json
Normal file
19
frontend/jsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
1554
frontend/package-lock.json
generated
Normal file
1554
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "vnshed",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
13
frontend/src/app.d.ts
vendored
Normal file
13
frontend/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
11
frontend/src/app.html
Normal file
11
frontend/src/app.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
1
frontend/src/lib/assets/favicon.svg
Normal file
1
frontend/src/lib/assets/favicon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
79
frontend/src/lib/components/add_button.svelte
Normal file
79
frontend/src/lib/components/add_button.svelte
Normal file
|
@ -0,0 +1,79 @@
|
|||
<script lang="ts">
|
||||
let { handler = () => {}, type = "button", files = $bindable() } = $props();
|
||||
|
||||
let file_list: FileList = $state([]);
|
||||
$effect(() => {
|
||||
// FileList -> Array[File]
|
||||
let f_list: Array<File> = [];
|
||||
for (let file of file_list)
|
||||
f_list.push(file);
|
||||
files = f_list;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if type == "button"}
|
||||
<button class="plus round" style="height: 32px;" onclick={handler}>+</button>
|
||||
{:else if type == "image"}
|
||||
<label for="img-upload" class="plus round" style="height: 48px; display: inline-block;">+</label>
|
||||
<input id="img-upload" accept="image/png, image/jpeg" style="display: none" bind:files={file_list} type="file"/>
|
||||
{:else}
|
||||
<label for="file-upload" class="file-plus rounded">Загрузить файл</label>
|
||||
<input id="file-upload" style="display: none" bind:files={file_list} multiple type="file"/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Кнопка "плюс" */
|
||||
.plus {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
aspect-ratio: 1 / 1;
|
||||
font-size: x-large;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.file-plus {
|
||||
font-size: larger;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
border: 0.15rem dashed;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.plus {
|
||||
background-color: #5C8DC0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.file-plus {
|
||||
background: none;
|
||||
border-color: #5C8DC0;
|
||||
color: #5C8DC0;
|
||||
}
|
||||
|
||||
.plus:hover {
|
||||
background-color: #567aa1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.plus {
|
||||
background-color: #2791FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.file-plus {
|
||||
background: none;
|
||||
border-color: #2791FF;
|
||||
color: #2791FF;
|
||||
}
|
||||
|
||||
.plus:hover {
|
||||
background-color: #2c7dd4;
|
||||
}
|
||||
}
|
||||
</style>
|
188
frontend/src/lib/components/calendar.svelte
Normal file
188
frontend/src/lib/components/calendar.svelte
Normal file
|
@ -0,0 +1,188 @@
|
|||
<script lang="ts">
|
||||
let current: Number = 9;
|
||||
|
||||
function set_day(day: Number) {
|
||||
current = day;
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="calendar input rounded">
|
||||
<div class="month">
|
||||
<ul>
|
||||
<li class="prev">❮</li>
|
||||
<li class="next">❯</li>
|
||||
<li>Август 2025</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="weekdays">
|
||||
{#each ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] as weekday}
|
||||
<li>{weekday}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<ul class="days">
|
||||
{#each [...Array(31).keys()] as day}
|
||||
<li>
|
||||
{#if current == day+1}
|
||||
<button class="active">{day+1}</button>
|
||||
{:else}
|
||||
<button class="nonactive" onclick={set_day(day+1)}>{day+1}</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
ul {list-style-type: none;}
|
||||
|
||||
.calendar {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.month {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.month ul {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.month ul li {
|
||||
display: inline;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.month .prev {
|
||||
float: inline-start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.month .next {
|
||||
float: inline-end;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.weekdays li {
|
||||
display: inline-block;
|
||||
width: 13.6%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.days {
|
||||
padding: 10px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.days li {
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
width: 13.6%;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
font-size: smaller;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.days li .active {
|
||||
height: 2em;
|
||||
padding: 5px;
|
||||
border-radius: 30%;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.days li .nonactive {
|
||||
height: 2em;
|
||||
padding: 5px;
|
||||
background: none;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.weekdays li, .days li {width: 13.1%;}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
.weekdays li, .days li {width: 12.5%;}
|
||||
.days li .active {padding: 2px;}
|
||||
.days li .nonactive {padding: 2px;}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 290px) {
|
||||
.weekdays li, .days li {width: 12.2%;}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.month {
|
||||
background: #5C8DC0;
|
||||
}
|
||||
|
||||
.month ul li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
.weekdays li {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.days {
|
||||
background: #F4F4F4;
|
||||
}
|
||||
|
||||
.days li {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.days li .active {
|
||||
color: white !important;
|
||||
background: #5C8DC0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.month {
|
||||
background: #2791FF;
|
||||
}
|
||||
|
||||
.month ul li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
background-color: #192431;
|
||||
}
|
||||
|
||||
.weekdays li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.days {
|
||||
background: #192431;
|
||||
}
|
||||
|
||||
.days li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.days li .active {
|
||||
color: white !important;
|
||||
background: #2791FF;
|
||||
}
|
||||
}
|
||||
</style>
|
23
frontend/src/lib/components/containers/files.svelte
Normal file
23
frontend/src/lib/components/containers/files.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import File from '$lib/components/file.svelte';
|
||||
import AddButton from '$lib/components/add_button.svelte';
|
||||
let selected = $state([]);
|
||||
let files: Array<File> = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if (selected.length > 0) {
|
||||
let added: Array<File> = [];
|
||||
for (let file of selected)
|
||||
added.push(file);
|
||||
files = files.concat(added);
|
||||
selected = [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="input rounded">
|
||||
{#each files as file}
|
||||
<File data={file}/>
|
||||
{/each}
|
||||
<AddButton type="file" bind:files={selected}/>
|
||||
</div>
|
24
frontend/src/lib/components/containers/images.svelte
Normal file
24
frontend/src/lib/components/containers/images.svelte
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import Image from "../image.svelte";
|
||||
|
||||
let { value = $bindable([]) } = $props();
|
||||
|
||||
</script>
|
||||
|
||||
<div class="img-conatiner rounded">
|
||||
<Image />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Горизонтальный список */
|
||||
.img-conatiner {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.img-conatiner :global(*) {
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
39
frontend/src/lib/components/containers/marks.svelte
Normal file
39
frontend/src/lib/components/containers/marks.svelte
Normal file
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import Mark from "../mark.svelte";
|
||||
import AddButton from "../add_button.svelte";
|
||||
|
||||
let { value = $bindable([]), label = "" } = $props();
|
||||
let items: string[] = $state([]);
|
||||
</script>
|
||||
|
||||
<div class="marks-container input rounded">
|
||||
<p>{label}</p>
|
||||
|
||||
{#each items as item}
|
||||
<Mark text={item}/>
|
||||
{/each}
|
||||
<AddButton handler={() => {
|
||||
items.push("");
|
||||
value = items;
|
||||
}} />
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Горизонтальный список */
|
||||
.marks-container {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
align-content: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.marks-container :global(*) {
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.marks-container p {
|
||||
align-content: center;
|
||||
}
|
||||
</style>
|
8
frontend/src/lib/components/containers/queue.svelte
Normal file
8
frontend/src/lib/components/containers/queue.svelte
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import QueueItem from '$lib/components/queue_item.svelte';
|
||||
</script>
|
||||
|
||||
<div class="input rounded">
|
||||
<QueueItem date="28.11.24" name="Fate/Stay Night (Судьба/Ночь схватки)"/>
|
||||
<QueueItem date="30.11.24" name="Fate/Hollow Ataraxia (Судьба/Святая атараксия)"/>
|
||||
</div>
|
69
frontend/src/lib/components/create_button.svelte
Normal file
69
frontend/src/lib/components/create_button.svelte
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!-- Кнопка "Создать" -->
|
||||
<script lang="ts">
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
function send_json() {
|
||||
fetch('http://127.0.0.1:8000/new', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': "<origin>"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="create">
|
||||
<button class="rounded" id="create" onclick={send_json}>Создать</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Поле с кнопкой "Создать" */
|
||||
.create {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.create button {
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.create {
|
||||
background-color: #D9D9D9;
|
||||
}
|
||||
|
||||
.create button {
|
||||
background-color: #3CAA36;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.create button:hover {
|
||||
background-color: #3b8d37;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.create {
|
||||
background-color: #192431;
|
||||
}
|
||||
|
||||
.create button {
|
||||
background-color: #3CAA36;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.create button:hover {
|
||||
background-color: #3b8d37;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
67
frontend/src/lib/components/file.svelte
Normal file
67
frontend/src/lib/components/file.svelte
Normal file
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
|
||||
const names = {
|
||||
0: ["B", 0],
|
||||
1: ["KB", 0],
|
||||
2: ["MB", 1],
|
||||
3: ["GB", 2],
|
||||
4: ["TB", 3]
|
||||
};
|
||||
|
||||
function size_str(size: number) {
|
||||
let iter = 0;
|
||||
while (size >= 1000) {
|
||||
iter++;
|
||||
size = size / 1000;
|
||||
}
|
||||
return size.toFixed(names[iter][1]).toString() + names[iter][0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="file rounded">
|
||||
<p class="filename">{data.name}</p>
|
||||
<p class="progress">{size_str(data.size)}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.file {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
height: 3rem;
|
||||
display: grid;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.file p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.file {
|
||||
background: #5C8DC0;
|
||||
}
|
||||
|
||||
.filename {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: #D4D4D4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.file {
|
||||
background: #2791FF;
|
||||
}
|
||||
|
||||
.filename {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: #D4D4D4;
|
||||
}
|
||||
}
|
||||
</style>
|
52
frontend/src/lib/components/image.svelte
Normal file
52
frontend/src/lib/components/image.svelte
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import AddButton from "./add_button.svelte";
|
||||
let { direction = "horisontal" } = $props();
|
||||
let images = $state();
|
||||
let image = $state();
|
||||
|
||||
$effect(() => {
|
||||
if (images.length > 0) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", function () {
|
||||
image.setAttribute("src", reader.result);
|
||||
});
|
||||
reader.readAsDataURL(images[0]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="{direction} input img-div rounded">
|
||||
<AddButton type="image" bind:files={images}/>
|
||||
<img bind:this={image} src="" alt="">
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Вертикальное изображение */
|
||||
.vertical {
|
||||
aspect-ratio: 11 / 16;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Горизонтальное изображение */
|
||||
.horisontal {
|
||||
aspect-ratio: 16 / 11;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.img-div) {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
:global(.img-div) img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(.img-div) :global(.plus) {
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
48
frontend/src/lib/components/img_button.svelte
Normal file
48
frontend/src/lib/components/img_button.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
let { style, image, name } = $props();
|
||||
</script>
|
||||
|
||||
<div class="imgbutton {style}">
|
||||
<img src={image} alt={name}>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Кнопка с изображением */
|
||||
.imgbutton {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.imgbutton img {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.imgbutton {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.remove {
|
||||
background-color: #FC1F1C;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
background-color: #d32522;
|
||||
}
|
||||
|
||||
.edit {
|
||||
background-color: #1BC304;
|
||||
}
|
||||
|
||||
.edit:hover {
|
||||
background-color: #1c9d0b;
|
||||
}
|
||||
</style>
|
94
frontend/src/lib/components/mark.svelte
Normal file
94
frontend/src/lib/components/mark.svelte
Normal file
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
let { text } = $props();
|
||||
|
||||
let compl_data = $state([]);
|
||||
|
||||
function autocomplete() {
|
||||
if (text !== "") {
|
||||
fetch('http://127.0.0.1:8000/api/marks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': "<origin>"
|
||||
},
|
||||
body: JSON.stringify({type: "tag", value: text})
|
||||
}).then((response) => response.json())
|
||||
.then((json) => compl_data = json);
|
||||
} else {
|
||||
compl_data = [];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<input type="text"
|
||||
class="dyn-input rounded"
|
||||
style="width: {text.length}ch;margin:0;"
|
||||
bind:value={text}
|
||||
oninput={autocomplete}>
|
||||
{#if compl_data}
|
||||
<div class="popup rounded" tabindex="0" role="button" onmousedown={(e) => {e.preventDefault();}}>
|
||||
{#each compl_data as item}
|
||||
<button onclick={text = this.textContent}>{item[2]}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
/* Элемент вертикального списка (тег, бадж, жанр) */
|
||||
.dyn-input {
|
||||
padding: 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.popup {
|
||||
text-align: center;
|
||||
display: none;
|
||||
position: absolute;
|
||||
margin-top: 0.5rem;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.popup * {
|
||||
background: none;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.dyn-input:focus + .popup {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.popup {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
.dyn-input {
|
||||
background-color: #5C8DC0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dyn-input:hover {
|
||||
background-color: #567aa1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.popup {
|
||||
background-color: #192431;
|
||||
}
|
||||
|
||||
.dyn-input {
|
||||
background-color: #2791FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dyn-input:hover {
|
||||
background-color: #2c7dd4;
|
||||
}
|
||||
}
|
||||
</style>
|
56
frontend/src/lib/components/queue_item.svelte
Normal file
56
frontend/src/lib/components/queue_item.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import { makeid } from "$lib/functions.js"
|
||||
import edit_btn from '$lib/assets/edit.svg';
|
||||
import remove_btn from '$lib/assets/delete.svg';
|
||||
import ImgButton from "./img_button.svelte";
|
||||
let id = makeid(8);
|
||||
let { date, name } = $props();
|
||||
</script>
|
||||
|
||||
<div class="queue-item rounded">
|
||||
<p>{date}</p>
|
||||
<p class="ellipsis">{name}</p>
|
||||
<ImgButton style="edit" name="Изменить" image={edit_btn}/>
|
||||
<ImgButton style="remove" name="Удалить" image={remove_btn}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Элемент в очереди */
|
||||
.queue-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.queue-item p {
|
||||
align-content: center;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
:global(.queue-item + .queue-item) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.queue-item {
|
||||
background-color: #5C8DC0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.queue-item p {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.queue-item {
|
||||
background-color: #2791FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.queue-item p {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
92
frontend/src/lib/css/global.css
Normal file
92
frontend/src/lib/css/global.css
Normal file
|
@ -0,0 +1,92 @@
|
|||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/font/Inter-Light.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Шрифт для всей страницы */
|
||||
* {
|
||||
font-family: Inter;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Все элементы для ввода данных (input, textarea, собственные) */
|
||||
.input {
|
||||
border: 0;
|
||||
resize: none;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Круглый элемент */
|
||||
.round {
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Закруглённый элемент */
|
||||
.rounded {
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Поле с обложкой */
|
||||
.cover {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
/* Поле с заполняемыми полями */
|
||||
.fields {
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: 1rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Отступ между полями */
|
||||
.fields .input {
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.fields p {
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.horisontal-bar {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.horisontal-bar p {
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
align-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Текст с троеточием в конце */
|
||||
.ellipsis {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
53
frontend/src/lib/css/themes.css
Normal file
53
frontend/src/lib/css/themes.css
Normal file
|
@ -0,0 +1,53 @@
|
|||
@media (prefers-color-scheme: light) {
|
||||
body {
|
||||
background-color: #EFEFF3;
|
||||
}
|
||||
|
||||
p {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #F4F4F4;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.cover .img-div {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.fields {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #0F1011;
|
||||
}
|
||||
|
||||
p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #192431;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cover .img-div {
|
||||
background-color: #131A22;
|
||||
}
|
||||
|
||||
.fields {
|
||||
background-color: #131A22;
|
||||
}
|
||||
}
|
46
frontend/src/lib/functions.js
Normal file
46
frontend/src/lib/functions.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
export function makeid(length) {
|
||||
var result = '';
|
||||
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var charactersLength = characters.length;
|
||||
for ( var i = 0; i < length; i++ ) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// const imageTypes = {
|
||||
// types: [
|
||||
// {
|
||||
// description: "Images",
|
||||
// accept: {
|
||||
// "image/*": [".png", ".gif", ".jpeg", ".jpg"],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// excludeAcceptAllOption: true,
|
||||
// multiple: false,
|
||||
// };
|
||||
|
||||
// let fileHandle;
|
||||
// export async function loadImage() {
|
||||
// [fileHandle] = await window.showOpenFilePicker(imageTypes);
|
||||
// return fileHandle;
|
||||
// }
|
||||
|
||||
// export function addImage(initiator, target) {
|
||||
// loadImage().then(async (result) => {
|
||||
// let img = document.createElement("img");
|
||||
// img.src = URL.createObjectURL(await result.getFile());
|
||||
// document.getElementById(target).appendChild(img);
|
||||
// initiator = document.getElementById(initiator);
|
||||
// if (initiator) initiator.remove();
|
||||
// //document.getElementById(target).addEventListener("click", addImage("", target));
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function json_filter(key,value)
|
||||
// {
|
||||
// if (key=="html") return undefined;
|
||||
// //else if (key=="id") return undefined;
|
||||
// else return value;
|
||||
// }
|
1
frontend/src/lib/index.js
Normal file
1
frontend/src/lib/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
80
frontend/src/routes/+layout.svelte
Normal file
80
frontend/src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,80 @@
|
|||
<script>
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import Calendar from '$lib/components/calendar.svelte';
|
||||
import CreateButton from '$lib/components/create_button.svelte';
|
||||
import Image from '$lib/components/image.svelte';
|
||||
|
||||
import FileContainer from '$lib/components/containers/files.svelte';
|
||||
import QueueContainer from '$lib/components/containers/queue.svelte';
|
||||
import MarksContainer from '$lib/components/containers/marks.svelte';
|
||||
import ImageContainer from '$lib/components/containers/images.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let page_data = $state({
|
||||
title: "",
|
||||
description: "",
|
||||
hours_to_read: 0,
|
||||
genres: [],
|
||||
tags: [],
|
||||
badges: []
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<!-- Обложка -->
|
||||
<div class="cover">
|
||||
<Image direction="vertical"/>
|
||||
<p>Обложка</p>
|
||||
</div>
|
||||
|
||||
<div class="fields rounded" id="fields">
|
||||
<!-- Название -->
|
||||
<input type="text" class="input rounded" placeholder="Название" bind:value={page_data["title"]}>
|
||||
|
||||
<!-- Описание -->
|
||||
<textarea class="input rounded" placeholder="Описание" rows="4" bind:value={page_data["description"]}></textarea>
|
||||
|
||||
<!-- Время на чтение -->
|
||||
<div class="horisontal-bar">
|
||||
<p>Время на чтение</p>
|
||||
<select class="input rounded" id="hours-count" bind:value={page_data["hours_to_read"]}>
|
||||
<option value="2">менее 2-х часов</option>
|
||||
<option value="10">2-10 часов</option>
|
||||
<option value="30">10-30 часов</option>
|
||||
<option value="50">50+ часов</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<MarksContainer label="Жанры" bind:value={page_data["genres"]}/>
|
||||
<MarksContainer label="Теги" bind:value={page_data["tags"]}/>
|
||||
<MarksContainer label="Баджи" bind:value={page_data["badges"]}/>
|
||||
|
||||
<p>Скриншоты</p>
|
||||
<ImageContainer />
|
||||
|
||||
<!-- Календарь с датой -->
|
||||
<p>Дата публикации</p>
|
||||
<Calendar />
|
||||
|
||||
<!-- Файлы -->
|
||||
<p>Файлы</p>
|
||||
<FileContainer />
|
||||
|
||||
<!-- Очередь публикации -->
|
||||
<p>Очередь публикации</p>
|
||||
<QueueContainer />
|
||||
|
||||
</div>
|
||||
|
||||
<CreateButton bind:data={page_data}/>
|
||||
|
||||
<style>
|
||||
@import '$lib/css/global.css';
|
||||
@import '$lib/css/themes.css';
|
||||
</style>
|
||||
|
||||
{@render children?.()}
|
2
frontend/src/routes/+page.svelte
Normal file
2
frontend/src/routes/+page.svelte
Normal file
|
@ -0,0 +1,2 @@
|
|||
<!-- <h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p> -->
|
2
frontend/static/robots.txt
Normal file
2
frontend/static/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
13
frontend/svelte.config.js
Normal file
13
frontend/svelte.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
6
frontend/vite.config.js
Normal file
6
frontend/vite.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
66
hmm.txt
Normal file
66
hmm.txt
Normal file
|
@ -0,0 +1,66 @@
|
|||
-- Эта таблица нужна, чтобы когда человек набирает в поле ввода тег или жанр или бадж,
|
||||
-- ему подсказывало уже существующие теги/жанры/баджи
|
||||
CREATE TABLE IF NOT EXISTS mark (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
-- "genre" or "tag" or "badge"
|
||||
tag TEXT NOT NULL,
|
||||
-- value, "romance", "Хохлы", as an example
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Таблица с авторами, для того же - чтобы подсказывало существующего автора.
|
||||
CREATE TABLE IF NOT EXISTS authors (
|
||||
-- ID автора, для кросс-референсов.
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
-- Имя автора.
|
||||
title TEXT NOT NULL,
|
||||
-- Описание автора.
|
||||
description TEXT NOT NULL,
|
||||
-- Путь к файлу обложки автора.
|
||||
thumbnail TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Сами новеллы. Нужно, поскольку несколько постов могут отсылаться к одной игре.
|
||||
CREATE TABLE IF NOT EXISTS novels (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
-- Название. Используется исключительно для поиска.
|
||||
title TEXT NOT NULL,
|
||||
author INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Таблица с **постами**.
|
||||
CREATE TABLE IF NOT EXISTS posts_log (
|
||||
-- Кросс-референс на id в novels.
|
||||
novel INTEGER NOT NULL,
|
||||
|
||||
-- ID канала, в который пост будет запощен.
|
||||
channel INTEGER NOT NULL,
|
||||
|
||||
-- массив idшников на записи в marks.
|
||||
marks JSON NOT NULL,
|
||||
|
||||
-- Название внки.
|
||||
title TEXT NOT NULL,
|
||||
|
||||
-- Описание ВНки.
|
||||
description TEXT NOT NULL,
|
||||
|
||||
-- id автора на момент планирования поста.
|
||||
author INTEGER NOT NULL,
|
||||
|
||||
-- Мапа, ключ - путь к файлу в фс, значение -
|
||||
-- `{"post": <POST_LINK>, "description": "описание файла"}`
|
||||
-- <POST_LINK> - ссылка на пост в соответствующем канале с файлами, может
|
||||
-- быть null. Не должно быть null, если post_info != NULL (то есть если уже запостили).
|
||||
files_on_disk JSON NOT NULL,
|
||||
|
||||
-- не NULL, если пост успешно запостили.
|
||||
-- {"link": <ссылка на пост>}
|
||||
post_info JSON,
|
||||
|
||||
-- Когда ВН должна быть запощена, second-precise unix timestamp.
|
||||
post_at INTEGER NOT NULL,
|
||||
|
||||
-- Когда запись в бд была создана, second-precise unix timestamp.
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
9
test_data.json
Normal file
9
test_data.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"title": "Higurashi no Naku Koro ni. Console-exclusive Arcs (Когда плачут цикады. Эксклюзивные главы)",
|
||||
"description": "Эксклюзивные главы для консольных версий \"Higurashi no Naku Koro ni\". Главы рекомендуется читать после ознакомления с оригинальными главами.\n\nUPD: На данный момент переведены: Taraimawashi, Tsukiotoshi, Hajisarashi, Miotsukushi и Материалы полиции\n\nОписание глав:\n\nTaraimawashi - Альтернативная первая глава, на первый взгляд — это дополнение к арке \"Вопросов\", пересказ Onikakushi-hen. Тем не менее, эта глава на самом деле содержит события Watanagashi-hen.\nУзнав секреты Хинамидзавы, Кейти решает игнорировать всё и наслаждаться мирной школьной жизнью.\nTsukiotoshi - Оригинальная консольная глава, которая является развилкой для третьей главы \"Tatarigoroshi\".\nДядя Сатоко приезжает в Хинамидзаву, и Кейти решает найти союзников, чтобы помочь Сатоко. Возможно, \"худший\" из миров, где на кубиках выпали одни \"единицы.\"\nHajisarashi - одним жарким днём Рика и её друзья идут в бассейн.\nMiotsukushi - альтернативная концовка всей оригинальной серии.\nМатериалы полиции - краткие истории по длине равных TIPS, которые немного раскрывают Оиши и Акасаку..[/i]",
|
||||
"hours_to_read": 50,
|
||||
"genres": [ "Драма", "Хоррор", "Детектив", "Мистика", "Повседневность" ],
|
||||
"tags": [ "Иностранный разработчик", "Несколько главных героев" ],
|
||||
"badges": [ "Лучшее" ],
|
||||
"post_at": "2024-04-13T08:30:00Z"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
class Novel:
|
||||
title: str
|
||||
description: str
|
||||
|
||||
vndb: int
|
||||
hours_to_read: int
|
||||
|
||||
tags: list[str]
|
||||
genres: list[str]
|
||||
|
||||
tg_post: str #url::Url
|
||||
post_at: datetime
|
||||
|
||||
class FullNovel:
|
||||
data: Novel
|
||||
|
||||
upload_queue: list[str]
|
||||
files: list[str]
|
||||
screenshots: list[str]
|
|
@ -1,75 +0,0 @@
|
|||
ul {list-style-type: none;}
|
||||
|
||||
.calendar * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.month {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.month ul {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.month ul li {
|
||||
display: inline;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.month .prev {
|
||||
float: inline-start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.month .next {
|
||||
float: inline-end;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.weekdays li {
|
||||
display: inline-block;
|
||||
width: 13.6%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.days {
|
||||
padding: 10px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.days li {
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
width: 13.6%;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
font-size: smaller;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.days li .active {
|
||||
padding: 5px;
|
||||
border-radius: 30%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.weekdays li, .days li {width: 13.1%;}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
.weekdays li, .days li {width: 12.5%;}
|
||||
.days li .active {padding: 2px;}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 290px) {
|
||||
.weekdays li, .days li {width: 12.2%;}
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../font/Inter-Light.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lobster';
|
||||
src: url('../font/Lobster-Regular.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Шрифт для всей страницы */
|
||||
* {
|
||||
font-family: Inter;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Все элементы для ввода данных (input, textarea, собственные) */
|
||||
.input {
|
||||
border: 0;
|
||||
resize: none;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Круглый элемент */
|
||||
.round {
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Закруглённый элемент */
|
||||
.rounded {
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Вертикальное изображение */
|
||||
.vertical {
|
||||
aspect-ratio: 11 / 16;
|
||||
}
|
||||
|
||||
/* Горизонтальное изображение */
|
||||
.horisontal {
|
||||
aspect-ratio: 16 / 11;
|
||||
}
|
||||
|
||||
.image {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
/* Кнопка "плюс" */
|
||||
.plus {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
aspect-ratio: 1 / 1;
|
||||
height: 32px;
|
||||
font-size: x-large;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Кнопка "плюс" на изображении должна быть больше */
|
||||
.image .plus {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
/* Поле с обложкой */
|
||||
.cover {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
/* Поле с кнопкой "Создать" */
|
||||
.create {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.create button {
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Поле с заполняемыми полями */
|
||||
.fields {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Отступ между полями */
|
||||
.fields * {
|
||||
margin: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Вертикальный список */
|
||||
.horisontal-bar {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
align-content: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.horisontal-bar * {
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
/* Элемент вертикального списка (тег, бадж, жанр) */
|
||||
.horisontal-item {
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Элемент в очереди */
|
||||
.queue-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.queue-item + .queue-item {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-item p {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
/* Текст с троеточием в конце */
|
||||
.ellipsis {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Кнопка с изображением */
|
||||
.imgbutton {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.imgbutton * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.imgbutton img {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
/* Кнопка "Удалить" */
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Кнопка "Изменить" */
|
||||
.edit {
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
@media (prefers-color-scheme: light) {
|
||||
body {
|
||||
background-color: #EFEFF3;
|
||||
}
|
||||
|
||||
p {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #F4F4F4;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.plus {
|
||||
background-color: #5C8DC0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plus:hover {
|
||||
background-color: #567aa1;
|
||||
}
|
||||
|
||||
.horisontal-item {
|
||||
background-color: #5C8DC0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.horisontal-item:hover {
|
||||
background-color: #567aa1;
|
||||
}
|
||||
|
||||
.cover .image {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.create {
|
||||
background-color: #D9D9D9;
|
||||
}
|
||||
|
||||
.create button {
|
||||
background-color: #3CAA36;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.create button:hover {
|
||||
background-color: #3b8d37;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fields {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
background-color: #5C8DC0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.queue-item p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.imgbutton {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.month {
|
||||
background: #5C8DC0;
|
||||
}
|
||||
|
||||
.month ul li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
.weekdays li {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.days {
|
||||
background: #F4F4F4;
|
||||
}
|
||||
|
||||
.days li {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.days li .active {
|
||||
color: white !important;
|
||||
background: #5C8DC0;
|
||||
}
|
||||
|
||||
.remove {
|
||||
background-color: #FC1F1C;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
background-color: #d32522;
|
||||
}
|
||||
|
||||
.edit {
|
||||
background-color: #1BC304;
|
||||
}
|
||||
|
||||
.edit:hover {
|
||||
background-color: #1c9d0b;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #0F1011;
|
||||
}
|
||||
|
||||
p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #192431;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plus {
|
||||
background-color: #2791FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plus:hover {
|
||||
background-color: #2c7dd4;
|
||||
}
|
||||
|
||||
.horisontal-item {
|
||||
background-color: #2791FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.horisontal-item:hover {
|
||||
background-color: #2c7dd4;
|
||||
}
|
||||
|
||||
.cover .image {
|
||||
background-color: #131A22;
|
||||
}
|
||||
|
||||
.create {
|
||||
background-color: #192431;
|
||||
}
|
||||
|
||||
.create button {
|
||||
background-color: #3CAA36;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.create button:hover {
|
||||
background-color: #3b8d37;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fields {
|
||||
background-color: #131A22;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
background-color: #2791FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.queue-item p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.imgbutton {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.month {
|
||||
background: #2791FF;
|
||||
}
|
||||
|
||||
.month ul li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
background-color: #192431;
|
||||
}
|
||||
|
||||
.weekdays li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.days {
|
||||
background: #192431;
|
||||
}
|
||||
|
||||
.days li {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.days li .active {
|
||||
color: white !important;
|
||||
background: #2791FF;
|
||||
}
|
||||
|
||||
.remove {
|
||||
background-color: #FC1F1C;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
background-color: #d32522;
|
||||
}
|
||||
|
||||
.edit {
|
||||
background-color: #1BC304;
|
||||
}
|
||||
|
||||
.edit:hover {
|
||||
background-color: #1c9d0b;
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Отложка</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/global.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/calendar.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/themes.css">
|
||||
<link rel="preload" href="font/Inter-Light.woff2" as="font" type="font/ttf" crossorigin>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="js/functions.js"></script>
|
||||
<script type="text/javascript" src="js/types.js"></script>
|
||||
|
||||
<!-- Обложка -->
|
||||
<div class="cover">
|
||||
<div class="vertical image rounded" id="cover-div">
|
||||
<button class="plus round" id="cover-plus">+</button>
|
||||
</div>
|
||||
<p>Обложка</p>
|
||||
</div>
|
||||
|
||||
<div class="fields rounded" id="fields">
|
||||
<!-- Название -->
|
||||
<input type="text" class="input rounded" placeholder="Название"></input>
|
||||
|
||||
<!-- Описание -->
|
||||
<textarea class="input rounded" placeholder="Описание" rows="4"></textarea>
|
||||
|
||||
<!-- Время на чтение -->
|
||||
<div class="horisontal-bar">
|
||||
<p>Время на чтение</p>
|
||||
<select class="input rounded" id="hours-count">
|
||||
<option value="first">менее 2-х часов</option>
|
||||
<option value="second">2-10 часов</option>
|
||||
<option value="third">10-30 часов</option>
|
||||
<option value="third">50+ часов</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Поле с жанрами -->
|
||||
<script>
|
||||
let genres = new HorisontalBar("Жанры", "genres");
|
||||
document.getElementById("fields").appendChild(genres.html);
|
||||
</script>
|
||||
|
||||
<!-- Поле с тегами -->
|
||||
<script>
|
||||
let tags = new HorisontalBar("Теги", "tags");
|
||||
document.getElementById("fields").appendChild(tags.html);
|
||||
</script>
|
||||
|
||||
<!-- Поле с баджами -->
|
||||
<script>
|
||||
let badges = new HorisontalBar("Баджи", "badges");
|
||||
document.getElementById("fields").appendChild(badges.html);
|
||||
</script>
|
||||
|
||||
<!-- Скриншоты -->
|
||||
<p>Скриншоты</p>
|
||||
<script>
|
||||
let screenshots = new HorisontalImageBar("screenshots");
|
||||
document.getElementById("fields").appendChild(screenshots.html);
|
||||
</script>
|
||||
|
||||
<!-- Календарь с датой -->
|
||||
<p>Дата публикации</p>
|
||||
<script>
|
||||
let calendar = new Calendar();
|
||||
document.getElementById("fields").appendChild(calendar.html);
|
||||
</script>
|
||||
|
||||
<!-- Очередь публикации -->
|
||||
<p>Очередь публикации</p>
|
||||
<div class="input rounded" id="queue">
|
||||
<script>
|
||||
let queue1 = new QueueItem("28.11.24", "Fate/Stay Night (Судьба/Ночь схватки)");
|
||||
document.getElementById("queue").appendChild(queue1.html);
|
||||
</script>
|
||||
<script>
|
||||
let queue2 = new QueueItem("30.11.24", "Fate/Hollow Ataraxia (Судьба/Святая атараксия)");
|
||||
document.getElementById("queue").appendChild(queue2.html);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка "Создать" -->
|
||||
<div class="create">
|
||||
<button class="rounded" id="create">Создать</button>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="js/index.js"></script>
|
||||
<script type="text/javascript" src="js/calendar.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
document.getElementById("cal-prev").addEventListener("click", (event) => {
|
||||
console.log("cal-prev");
|
||||
})
|
||||
|
||||
document.getElementById("cal-next").addEventListener("click", (event) => {
|
||||
console.log("cal-next");
|
||||
})
|
|
@ -1,46 +0,0 @@
|
|||
function makeid(length) {
|
||||
var result = '';
|
||||
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var charactersLength = characters.length;
|
||||
for ( var i = 0; i < length; i++ ) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const imageTypes = {
|
||||
types: [
|
||||
{
|
||||
description: "Images",
|
||||
accept: {
|
||||
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
|
||||
},
|
||||
},
|
||||
],
|
||||
excludeAcceptAllOption: true,
|
||||
multiple: false,
|
||||
};
|
||||
|
||||
let fileHandle;
|
||||
async function loadImage() {
|
||||
[fileHandle] = await window.showOpenFilePicker(imageTypes);
|
||||
return fileHandle;
|
||||
}
|
||||
|
||||
function addImage(initiator, target) {
|
||||
loadImage().then(async (result) => {
|
||||
let img = document.createElement("img");
|
||||
img.src = URL.createObjectURL(await result.getFile());
|
||||
document.getElementById(target).appendChild(img);
|
||||
initiator = document.getElementById(initiator);
|
||||
if (initiator) initiator.remove();
|
||||
//document.getElementById(target).addEventListener("click", addImage("", target));
|
||||
});
|
||||
}
|
||||
|
||||
function json_filter(key,value)
|
||||
{
|
||||
if (key=="html") return undefined;
|
||||
//else if (key=="id") return undefined;
|
||||
else return value;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
document.getElementById("cover-plus").addEventListener("click", (event) => {
|
||||
addImage("cover-plus", "cover-div");
|
||||
})
|
||||
|
||||
// document.getElementById("screen-plus").addEventListener("click", (event) => {
|
||||
// addImage("screen-plus", "scr-1");
|
||||
// })
|
||||
|
||||
document.getElementById("create").addEventListener("click", (event) => {
|
||||
console.log("create");
|
||||
})
|
249
www/js/types.js
249
www/js/types.js
|
@ -1,249 +0,0 @@
|
|||
class AddButton {
|
||||
|
||||
id = "";
|
||||
html = undefined;
|
||||
|
||||
constructor() {
|
||||
this.html = document.createElement('button');
|
||||
this.html.classList.add("plus");
|
||||
this.html.classList.add("round");
|
||||
this.html.appendChild(document.createTextNode("+"));
|
||||
this.id = makeid(8);
|
||||
this.html.id = this.id;
|
||||
}
|
||||
|
||||
addHadler(func) {
|
||||
this.html.addEventListener("click", func);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class HorisontalItem {
|
||||
|
||||
id = undefined;
|
||||
html = undefined;
|
||||
text = undefined;
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("horisontal-item");
|
||||
this.html.classList.add("rounded");
|
||||
this.html.appendChild(document.createTextNode(text));
|
||||
this.id = makeid(8);
|
||||
this.html.id = this.id;
|
||||
this.html.addEventListener("click", this.remove);
|
||||
}
|
||||
|
||||
remove() {
|
||||
document.getElementById(this.id).remove();
|
||||
}
|
||||
}
|
||||
|
||||
class HorisontalBar {
|
||||
|
||||
html = undefined;
|
||||
plus_id = undefined;
|
||||
elements = [];
|
||||
|
||||
constructor(text, id) {
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("horisontal-bar");
|
||||
this.html.classList.add("input");
|
||||
this.html.classList.add("rounded");
|
||||
let label = document.createElement('p');
|
||||
label.appendChild(document.createTextNode(text))
|
||||
this.html.appendChild(label);
|
||||
const plus = new AddButton().addHadler(
|
||||
() => this.append(new HorisontalItem('test'))
|
||||
);
|
||||
this.html.appendChild(plus.html);
|
||||
this.plus_id = plus.id;
|
||||
}
|
||||
|
||||
append(elem) {
|
||||
this.elements.push(elem);
|
||||
this.html.insertBefore(elem.html, document.getElementById(this.plus_id));
|
||||
return this;
|
||||
}
|
||||
|
||||
json() {
|
||||
return JSON.stringify(this.elements, json_filter);
|
||||
}
|
||||
}
|
||||
|
||||
class HorisontalImage {
|
||||
|
||||
id = undefined;
|
||||
html = undefined;
|
||||
|
||||
constructor() {
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("horisontal");
|
||||
this.html.classList.add("image");
|
||||
this.html.classList.add("input");
|
||||
this.html.classList.add("rounded");
|
||||
this.id = makeid(8);
|
||||
this.html.id = this.id;
|
||||
|
||||
let plus = new AddButton();
|
||||
plus.addHadler(() => {
|
||||
addImage(plus.id, this.id);
|
||||
document.getElementById("screenshots").appendChild(new HorisontalImage().html);
|
||||
});
|
||||
this.html.appendChild(plus.html);
|
||||
}
|
||||
|
||||
remove() {
|
||||
document.getElementById(this.id).remove();
|
||||
}
|
||||
}
|
||||
|
||||
class HorisontalImageBar {
|
||||
|
||||
html = undefined;
|
||||
first_id = undefined;
|
||||
elements = [];
|
||||
|
||||
constructor(id) {
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("horisontal-bar");
|
||||
const first = new HorisontalImage();
|
||||
this.html.appendChild(first.html);
|
||||
this.first_id = first.id;
|
||||
}
|
||||
|
||||
append(elem) {
|
||||
this.elements.push(elem);
|
||||
this.html.insertBefore(elem.html, document.getElementById(this.first_id));
|
||||
return this;
|
||||
}
|
||||
|
||||
json() {
|
||||
return JSON.stringify(this.elements, json_filter);
|
||||
}
|
||||
}
|
||||
|
||||
class ImgButton {
|
||||
|
||||
html = undefined;
|
||||
id = undefined;
|
||||
|
||||
constructor(name, img) {
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("imgbutton");
|
||||
this.id = makeid(8);
|
||||
this.html.id = this.id;
|
||||
|
||||
let btn_img = document.createElement('img');
|
||||
btn_img.src = img;
|
||||
btn_img.innerHTML = name;
|
||||
this.html.appendChild(btn_img);
|
||||
}
|
||||
|
||||
addHadler(func) {
|
||||
this.html.addEventListener("click", func);
|
||||
return this;
|
||||
}
|
||||
|
||||
setCssClass(name) {
|
||||
this.html.classList.add(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class QueueItem {
|
||||
|
||||
html = undefined;
|
||||
id = undefined;
|
||||
|
||||
constructor(date, name) {
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("queue-item");
|
||||
this.html.classList.add("horisontal-bar");
|
||||
this.html.classList.add("rounded");
|
||||
this.id = makeid(8);
|
||||
this.html.id = this.id;
|
||||
|
||||
var date_p = document.createElement('p');
|
||||
date_p.innerHTML = date;
|
||||
this.html.appendChild(date_p);
|
||||
|
||||
var text = document.createElement('p');
|
||||
text.classList.add("ellipsis");
|
||||
text.innerHTML = name;
|
||||
this.html.appendChild(text);
|
||||
|
||||
let edit_btn = new ImgButton("Изменить", "./img/edit.svg");
|
||||
edit_btn.setCssClass("edit");
|
||||
this.html.appendChild(edit_btn.html);
|
||||
let rem_btn = new ImgButton("Удалить", "./img/delete.svg");
|
||||
rem_btn.setCssClass("remove");
|
||||
this.html.appendChild(rem_btn.html);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Calendar {
|
||||
|
||||
html = undefined;
|
||||
id = undefined;
|
||||
|
||||
constructor() {
|
||||
this.html = document.createElement('div');
|
||||
this.html.classList.add("calendar");
|
||||
this.html.classList.add("rounded");
|
||||
this.id = makeid(8);
|
||||
this.html.id = this.id;
|
||||
|
||||
// Calendar Header
|
||||
let month = document.createElement('div');
|
||||
month.classList.add("month");
|
||||
let m_header = document.createElement('ul');
|
||||
let prev_btn = document.createElement('li');
|
||||
prev_btn.classList.add("prev");
|
||||
prev_btn.id = this.id + "-prev";
|
||||
prev_btn.innerHTML = "❮";
|
||||
m_header.appendChild(prev_btn);
|
||||
let next_btn = document.createElement('li');
|
||||
next_btn.classList.add("next");
|
||||
next_btn.id = this.id + "-next";
|
||||
next_btn.innerHTML = "❯";
|
||||
m_header.appendChild(next_btn);
|
||||
let month_text = document.createElement('li');
|
||||
month_text.innerHTML = "Август 2025";
|
||||
m_header.appendChild(month_text);
|
||||
month.appendChild(m_header);
|
||||
this.html.appendChild(month);
|
||||
|
||||
// Week header
|
||||
let weekdays = document.createElement('ul');
|
||||
weekdays.classList.add("weekdays");
|
||||
const days = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||||
for (const day of days) {
|
||||
let day_li = document.createElement('li');
|
||||
day_li.innerHTML = day;
|
||||
weekdays.appendChild(day_li);
|
||||
}
|
||||
this.html.appendChild(weekdays);
|
||||
|
||||
let day_table = document.createElement('ul');
|
||||
day_table.classList.add("days");
|
||||
const current = 10;
|
||||
for (const day of [...Array(31).keys()]) {
|
||||
let day_li = document.createElement('li');
|
||||
if (day+1 == current) {
|
||||
let day_sp = document.createElement('span');
|
||||
day_sp.classList.add("active");
|
||||
day_sp.innerHTML = day+1;
|
||||
day_li.appendChild(day_sp);
|
||||
} else {
|
||||
day_li.innerHTML = day+1;
|
||||
}
|
||||
day_table.appendChild(day_li);
|
||||
}
|
||||
this.html.appendChild(day_table);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue