Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
ed0bc5cb1f | |||
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 3125 additions and 935 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,2 +1,7 @@
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
.idea/
|
.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>
|
255
frontend/src/lib/components/calendar.svelte
Normal file
255
frontend/src/lib/components/calendar.svelte
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
let monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||||
|
|
||||||
|
let headers: Array<string> = [];
|
||||||
|
let now = new Date();
|
||||||
|
let year = now.getFullYear(); // this is the month & year displayed
|
||||||
|
let month = now.getMonth();
|
||||||
|
let current = now.getDay();
|
||||||
|
var days: Array<Object> = []; // The days to display in each box
|
||||||
|
|
||||||
|
$: month,year,initContent();
|
||||||
|
|
||||||
|
// choose what date/day gets displayed in each date box.
|
||||||
|
function initContent() {
|
||||||
|
headers = dayNames;
|
||||||
|
initMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMonth() {
|
||||||
|
days = [];
|
||||||
|
// find the last Monday of the previous month
|
||||||
|
var firstDay = new Date(year, month, 1).getDay();
|
||||||
|
//console.log('fd='+firstDay+' '+dayNames[firstDay]);
|
||||||
|
var daysInThisMonth = new Date(year, month+1, 0).getDate();
|
||||||
|
var daysInLastMonth = new Date(year, month, 0).getDate();
|
||||||
|
var prevMonth = month==0 ? 11 : month-1;
|
||||||
|
|
||||||
|
// show the days before the start of this month (disabled) - always less than 7
|
||||||
|
for (let i=daysInLastMonth-firstDay;i<daysInLastMonth;i++) {
|
||||||
|
let d = new Date(prevMonth==11?year-1:year,prevMonth,i+1);
|
||||||
|
days.push({name:''+(i+1),enabled:false,date:d,});
|
||||||
|
}
|
||||||
|
// show the days in this month (enabled) - always 28 - 31
|
||||||
|
for (let i=0;i<daysInThisMonth;i++) {
|
||||||
|
let d = new Date(year,month,i+1);
|
||||||
|
if (i==0) days.push({name:(i+1),enabled:true,date:d,});
|
||||||
|
else days.push({name:''+(i+1),enabled:true,date:d,});
|
||||||
|
//console.log('i='+i+' dt is '+d+' date() is '+d.getDate());
|
||||||
|
}
|
||||||
|
// show any days to fill up the last row (disabled) - always less than 7
|
||||||
|
for (let i=0;days.length%7;i++) {
|
||||||
|
let d = new Date((month==11?year+1:year),(month+1)%12,i+1);
|
||||||
|
if (i==0) days.push({name:(i+1),enabled:false,date:d,});
|
||||||
|
else days.push({name:''+(i+1),enabled:false,date:d,});
|
||||||
|
}
|
||||||
|
console.log(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
month++;
|
||||||
|
if (month == 12) {
|
||||||
|
year++;
|
||||||
|
month=0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function prev() {
|
||||||
|
if (month==0) {
|
||||||
|
month=11;
|
||||||
|
year--;
|
||||||
|
} else {
|
||||||
|
month--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected: Object;
|
||||||
|
|
||||||
|
function set_day(day: Object) {
|
||||||
|
selected = day;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="calendar input rounded">
|
||||||
|
<div class="month">
|
||||||
|
<ul>
|
||||||
|
<button class="prev" onclick={prev()}>❮</button>
|
||||||
|
<button class="next" onclick={next()}>❯</button>
|
||||||
|
<li>{monthNames[month]} {year}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ul class="weekdays">
|
||||||
|
{#each headers as weekday}
|
||||||
|
<li>{weekday}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<ul class="days">
|
||||||
|
{#each days as day}
|
||||||
|
<li>
|
||||||
|
{#if selected == day}
|
||||||
|
<button class="active">{day.name}</button>
|
||||||
|
{:else}
|
||||||
|
<button class="nonactive" onclick={set_day(day)}>{day.name}</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;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month .next {
|
||||||
|
float: inline-end;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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