Compare commits
27
Commits
1cc6b7b1ba
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93812b818d | ||
|
|
741af85d86 | ||
|
|
5429166334 | ||
|
|
79b667b644 | ||
|
|
443e4e4d5c | ||
|
|
e16a40148a | ||
|
|
1a9eb9299b | ||
|
|
36ce59c4ab | ||
|
|
02546c12f1 | ||
|
|
c105d878fe | ||
|
|
8e6ce7f6c4 | ||
|
|
108d1bac5c | ||
|
|
0402f2d2b7 | ||
|
|
4c02b47620 | ||
|
|
4893c8ddb0 | ||
|
|
fe0314015c | ||
|
|
06001b29f8 | ||
|
|
bb70f3432d | ||
|
|
cabe17884a | ||
|
|
9e0facdf83 | ||
|
|
8941f625c8 | ||
|
|
44e1415194 | ||
|
|
3c0a763572 | ||
|
|
20bf3a0c34 | ||
|
|
14350f84c5 | ||
|
|
7e335c51c6 | ||
|
|
9f7fd6e5e7 |
@@ -4,9 +4,16 @@ A self-host, multi-account calendar system.
|
|||||||
|
|
||||||
## Warning
|
## Warning
|
||||||
|
|
||||||
This project still work in progress. Because this project need a massive refactor now.
|
This project is **NOT** suit for any cases in production environment.
|
||||||
If you want to check out the first version which can fufill basic usage, please switch to `v1-maintain` branch. In `main` branch, I am refactoring v1 and it will be updated to v2 in future.
|
It is just a toybox where I learn modern frontend and backend.
|
||||||
The first version of this project have too much C-style JavaScript. It is too complicated to maintain and cannot add any other new features. Therefore, it needs to be fully refactored using ES6 and some modern JavaScript tools. It will come soon.
|
|
||||||
|
## Version Infos
|
||||||
|
|
||||||
|
If you want to check out the first version which can fufill basic usage, please switch to `v1-maintain` branch.
|
||||||
|
It is fully written in Python Flask backend and jQuery frontend.
|
||||||
|
|
||||||
|
In `master` branch, I am developing with Go backend and Vue frontend.
|
||||||
|
And I also learn them from it.
|
||||||
|
|
||||||
## Features & shortcomings
|
## Features & shortcomings
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -1,10 +1,10 @@
|
|||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
1. 前后端分离,将前端静态文件和后端Python分装到两个文件夹中。同时辅助类文件夹改换位置。
|
- [x] 前后端分离,将前端静态文件和后端Python分装到两个文件夹中。同时辅助类文件夹改换位置。
|
||||||
1. 后端使用Astral UV重构,使得项目可以跑起来。
|
- [x] 后端使用Astral UV重构,使得项目可以跑起来。
|
||||||
1. 定为1.1版本。1.0版本也要打tag然后提交。然后分叉v1-maintain分支。后续在master上开发v2。
|
- [x] 定为1.1版本。1.0版本也要打tag然后提交。然后分叉v1-maintain分支。后续在master上开发v2。
|
||||||
1. 后端数据库字段重命名。
|
- [x] 后端数据库字段重命名。
|
||||||
1. 前后端通信API命名格式修改。
|
- [ ] 前后端通信API命名格式修改。
|
||||||
1. 使用Vue重写前端
|
- [x] 使用Vue重写前端
|
||||||
1. 使用Tailwind重写前端CSS
|
- [ ] 使用Tailwind重写前端CSS
|
||||||
1. 使用Go重写后端。
|
- [x] 使用Go重写后端。
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
# Driver: "sqlite" or "mysql"
|
||||||
[database]
|
[database]
|
||||||
driver = "sqlite"
|
driver = "sqlite"
|
||||||
|
|
||||||
|
# Path to the SQLite database file
|
||||||
[database.config]
|
[database.config]
|
||||||
path = "coconut-leaf.db"
|
path = "coconut-leaf.db"
|
||||||
|
|
||||||
|
# MySQL connection parameters (placeholder, not yet implemented)
|
||||||
# [database]
|
# [database]
|
||||||
# driver = "mysql"
|
# driver = "mysql"
|
||||||
#
|
#
|
||||||
# [database.config]
|
# [database.config]
|
||||||
# host = "localhost"
|
# host = "localhost"
|
||||||
# port = 3306
|
# port = 3306
|
||||||
@@ -14,9 +17,12 @@ path = "coconut-leaf.db"
|
|||||||
# password = "password"
|
# password = "password"
|
||||||
# database = "coconut_leaf"
|
# database = "coconut_leaf"
|
||||||
|
|
||||||
|
# HTTP listening port
|
||||||
[web]
|
[web]
|
||||||
port = 8848
|
port = 8848
|
||||||
|
|
||||||
[others]
|
[others]
|
||||||
|
# Interval (in seconds) for automatic cleanup of expired tokens
|
||||||
auto-token-clean-duration = 86400
|
auto-token-clean-duration = 86400
|
||||||
|
# Debug mode; must be set to false in production
|
||||||
debug = true
|
debug = true
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
# Setup this path to coconut-leaf legacy frontend.
|
||||||
|
set $frontend_root /var/www/coconut-leaf/frontend-legacy;
|
||||||
|
root $frontend_root;
|
||||||
|
index home.html;
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Route 1: /web -> HTML templates with extensionless fallback
|
||||||
|
# ============================
|
||||||
|
location /web {
|
||||||
|
# Use alias to exactly mapping
|
||||||
|
alias $frontend_root/templates;
|
||||||
|
|
||||||
|
# Optimize for static file
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
gzip_static on;
|
||||||
|
|
||||||
|
# Try file with .html suffix
|
||||||
|
try_files $uri.html $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /web/eventAdd -> templates/event.html, replace {{uuidPath}} with blank
|
||||||
|
location = /web/eventAdd {
|
||||||
|
alias $frontend_root/templates/event.html;
|
||||||
|
default_type text/html;
|
||||||
|
sub_filter '{{uuidPath}}' '';
|
||||||
|
sub_filter_once on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /web/eventUpdate/<path:uuidPath> -> templates/event.html, replace {{uuidPath}} with uuidPath
|
||||||
|
location ~ ^/web/eventUpdate/(?<uuidPath>.+)$ {
|
||||||
|
alias $frontend_root/templates/event.html;
|
||||||
|
default_type text/html;
|
||||||
|
sub_filter '{{uuidPath}}' $uuidPath;
|
||||||
|
sub_filter_once on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Route 2: /static -> static files
|
||||||
|
# ============================
|
||||||
|
location /static {
|
||||||
|
# Use alias to exactly mapping
|
||||||
|
alias $frontend_root/static;
|
||||||
|
|
||||||
|
# Optimize for static file
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
gzip_static on;
|
||||||
|
|
||||||
|
# Try file with .html suffix
|
||||||
|
try_files $uri $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Route 3: /api -> reverse proxy to backend
|
||||||
|
# ============================
|
||||||
|
location /api {
|
||||||
|
# Rewrite to remove "api" prefix
|
||||||
|
rewrite ^/api(/.*)$ $1 break;
|
||||||
|
|
||||||
|
# Reverse proxy to localhost backend
|
||||||
|
proxy_pass http://127.0.0.1:8848;
|
||||||
|
|
||||||
|
# Keep original request headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Optional: WebSocket support
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Time configuration
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
# Buffer setup (changed when meeting large file uploading)
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 8 4k;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================
|
||||||
|
# Root path redirect: Root -> /web/home
|
||||||
|
# ============================
|
||||||
|
location = / {
|
||||||
|
return 302 /web/home;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny hidden files at root level
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
@@ -1,61 +1,64 @@
|
|||||||
|
|
||||||
|
# Setup this path to coconut-leaf frontend.
|
||||||
|
set $frontend_root /var/www/coconut-leaf/frontend/dist;
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
# 路由 1: /web -> 静态文件
|
# Route 1: /web -> Vite-built SPA
|
||||||
# ============================
|
# ============================
|
||||||
location /web {
|
location /web {
|
||||||
# 使用 alias 精确映射
|
alias $frontend_root;
|
||||||
# 请求 /web/index.html -> /var/www/static/index.html
|
index index.html;
|
||||||
alias /var/www/static;
|
|
||||||
|
# Optimize for static file
|
||||||
# 静态文件优化
|
|
||||||
expires 7d;
|
expires 7d;
|
||||||
add_header Cache-Control "public, max-age=604800";
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
|
||||||
# 尝试返回文件,不存在则返回404(避免落入其他location)
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
|
|
||||||
# 可选:启用 gzip 压缩
|
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
|
|
||||||
|
# SPA fallback: for non-file routes, serve index.html
|
||||||
|
try_files $uri $uri/ /web/index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
# 路由 2: /api -> Go 程序 (8848端口)
|
# Route 3: /api -> reverse proxy to backend
|
||||||
# ============================
|
# ============================
|
||||||
location /api {
|
location /api {
|
||||||
# 反向代理到本地 Go 服务
|
# Rewrite to remove "api" prefix
|
||||||
|
rewrite ^/api(/.*)$ $1 break;
|
||||||
|
|
||||||
|
# Reverse proxy to localhost backend
|
||||||
proxy_pass http://127.0.0.1:8848;
|
proxy_pass http://127.0.0.1:8848;
|
||||||
|
|
||||||
# 重要:保留原始请求头
|
# Keep original request headers
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# WebSocket 支持(如果 Go 程序需要)
|
# Optional: WebSocket support
|
||||||
# proxy_http_version 1.1;
|
# proxy_http_version 1.1;
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
# proxy_set_header Connection "upgrade";
|
# proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
# 超时设置(根据业务调整)
|
# Time configuration
|
||||||
proxy_connect_timeout 60s;
|
proxy_connect_timeout 60s;
|
||||||
proxy_send_timeout 60s;
|
proxy_send_timeout 60s;
|
||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
# 缓冲设置(可选,大文件上传时注意调整)
|
# Buffer setup (changed when meeting large file uploading)
|
||||||
proxy_buffering on;
|
proxy_buffering on;
|
||||||
proxy_buffer_size 4k;
|
proxy_buffer_size 4k;
|
||||||
proxy_buffers 8 4k;
|
proxy_buffers 8 4k;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
# 可选:根路径处理
|
# Root path redirect: Root -> /web/home
|
||||||
# ============================
|
# ============================
|
||||||
location = / {
|
location = / {
|
||||||
# 重定向到 /web
|
return 302 /web/home;
|
||||||
return 302 /web/;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 禁止访问隐藏文件
|
# Deny hidden files at root level
|
||||||
location ~ /\. {
|
location ~ /\. {
|
||||||
deny all;
|
deny all;
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
## ======== Personal ========
|
|
||||||
# Database file
|
|
||||||
*.db
|
|
||||||
|
|
||||||
# Ignore setting file
|
|
||||||
coconut-leaf.toml
|
|
||||||
|
|
||||||
## ======== Python ========
|
|
||||||
# Python-generated files
|
|
||||||
__pycache__/
|
|
||||||
*.py[oc]
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
wheels/
|
|
||||||
*.egg-info
|
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
.venv
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.11
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import sys
|
|
||||||
from argparse import ArgumentParser
|
|
||||||
from typing import cast
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import server
|
|
||||||
import config
|
|
||||||
import utils
|
|
||||||
import database
|
|
||||||
import logger
|
|
||||||
from logger import LOGGER, LoggerLevel
|
|
||||||
|
|
||||||
|
|
||||||
def GetUsernamePassword() -> tuple[str, str]:
|
|
||||||
print("What is the first username of this calendar system?")
|
|
||||||
cache = input()
|
|
||||||
while not utils.IsValidUsername(cache):
|
|
||||||
print("Sorry, invalid data. Please try again.")
|
|
||||||
cache = input()
|
|
||||||
username = cache
|
|
||||||
|
|
||||||
print("Input this user password:")
|
|
||||||
cache = input()
|
|
||||||
while not utils.IsValidPassword(cache):
|
|
||||||
print("Sorry, invalid data. Please try again.")
|
|
||||||
cache = input()
|
|
||||||
password = cache
|
|
||||||
|
|
||||||
return (username, password)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Set as INFO level in default first,
|
|
||||||
# and we will change it once we load the configuration file.
|
|
||||||
logger.set_level(LoggerLevel.INFO)
|
|
||||||
|
|
||||||
# Receive arguments
|
|
||||||
parser = ArgumentParser(
|
|
||||||
description="The server of light, self-host and multi-account calendar system."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--config",
|
|
||||||
required=True,
|
|
||||||
type=Path,
|
|
||||||
action="store",
|
|
||||||
metavar="CONFIG_TOML",
|
|
||||||
dest="config",
|
|
||||||
help="The configuration file for coconut-leaf",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--init",
|
|
||||||
action="store_true",
|
|
||||||
dest="init",
|
|
||||||
help="Set for initialize the calendar system",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Show splash
|
|
||||||
LOGGER.info("Coconut-leaf")
|
|
||||||
LOGGER.info("A light, self-host and multi-account calendar system")
|
|
||||||
LOGGER.info("Project: https://github.com/yyc12345/coconut-leaf")
|
|
||||||
LOGGER.info("===================")
|
|
||||||
|
|
||||||
# Load config file
|
|
||||||
try:
|
|
||||||
config.setup_config(cast(Path, args.config))
|
|
||||||
except Exception as e:
|
|
||||||
LOGGER.critical(f"Error loading config file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Change logging level again according to whether enable debug mode
|
|
||||||
logging_level = LoggerLevel.DEBUG if config.get_config().others.debug else LoggerLevel.INFO
|
|
||||||
logger.set_level(logging_level)
|
|
||||||
|
|
||||||
# Initialize the calendar system if needed
|
|
||||||
if cast(bool, args.init):
|
|
||||||
gotten_data = GetUsernamePassword()
|
|
||||||
calendar = database.CalendarDatabase()
|
|
||||||
calendar.init(*gotten_data)
|
|
||||||
calendar.close()
|
|
||||||
|
|
||||||
LOGGER.info("Staring server...")
|
|
||||||
server.run()
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import tomllib
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import StrEnum
|
|
||||||
from typing import Optional
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseDriver(StrEnum):
|
|
||||||
SQLITE = "sqlite"
|
|
||||||
MYSQL = "mysql"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
return DatabaseDriver(raw["driver"])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SqliteDatabaseConfig:
|
|
||||||
path: str
|
|
||||||
"""Database path"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
return SqliteDatabaseConfig(raw["path"])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class MysqlDatabaseConfig:
|
|
||||||
host: str
|
|
||||||
"""Database host"""
|
|
||||||
port: int
|
|
||||||
"""Database port"""
|
|
||||||
user: str
|
|
||||||
"""Database user"""
|
|
||||||
password: str
|
|
||||||
"""Database password"""
|
|
||||||
database: str
|
|
||||||
"""Database name"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
return MysqlDatabaseConfig(
|
|
||||||
raw["host"], raw["port"], raw["user"], raw["password"], raw["database"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DatabaseConfig:
|
|
||||||
driver: DatabaseDriver
|
|
||||||
"""Database driver"""
|
|
||||||
config: SqliteDatabaseConfig | MysqlDatabaseConfig
|
|
||||||
"""Database config"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
if raw["driver"] == DatabaseDriver.SQLITE:
|
|
||||||
return DatabaseConfig(
|
|
||||||
DatabaseDriver.SQLITE, SqliteDatabaseConfig.from_raw(raw["config"])
|
|
||||||
)
|
|
||||||
elif raw["driver"] == DatabaseDriver.MYSQL:
|
|
||||||
return DatabaseConfig(
|
|
||||||
DatabaseDriver.MYSQL, MysqlDatabaseConfig.from_raw(raw["config"])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid database driver")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class WebConfig:
|
|
||||||
port: int
|
|
||||||
"""Web server port"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
return WebConfig(raw["port"])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class OthersConfig:
|
|
||||||
debug: bool
|
|
||||||
"""Whether enable debug mode"""
|
|
||||||
auto_token_clean_duration: int
|
|
||||||
"""Auto token clean duration"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
return OthersConfig(raw["debug"], raw["auto-token-clean-duration"])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Config:
|
|
||||||
database: DatabaseConfig
|
|
||||||
web: WebConfig
|
|
||||||
others: OthersConfig
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_raw(raw: dict):
|
|
||||||
return Config(
|
|
||||||
database=DatabaseConfig.from_raw(raw["database"]),
|
|
||||||
web=WebConfig.from_raw(raw["web"]),
|
|
||||||
others=OthersConfig.from_raw(raw["others"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_CONFIG: Optional[Config] = None
|
|
||||||
|
|
||||||
|
|
||||||
def setup_config(p: Path) -> None:
|
|
||||||
"""
|
|
||||||
Setup config by given path.
|
|
||||||
|
|
||||||
Raise exception if config file is invalid.
|
|
||||||
"""
|
|
||||||
with open(p, "rb") as f:
|
|
||||||
raw = tomllib.load(f)
|
|
||||||
|
|
||||||
global _CONFIG
|
|
||||||
_CONFIG = Config.from_raw(raw)
|
|
||||||
|
|
||||||
|
|
||||||
def get_config() -> Config:
|
|
||||||
"""
|
|
||||||
Get config instance.
|
|
||||||
|
|
||||||
Raises RuntimeError if config is not loaded.
|
|
||||||
"""
|
|
||||||
if _CONFIG is None:
|
|
||||||
raise RuntimeError("Config is not loaded. Call setup_config() first.")
|
|
||||||
else:
|
|
||||||
return _CONFIG
|
|
||||||
@@ -1,708 +0,0 @@
|
|||||||
import sqlite3
|
|
||||||
import threading
|
|
||||||
from typing import cast
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Callable, ParamSpec, TypeVar, Generic
|
|
||||||
|
|
||||||
import dt
|
|
||||||
import utils
|
|
||||||
import config
|
|
||||||
from logger import LOGGER
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
P = ParamSpec('P')
|
|
||||||
R = TypeVar('R')
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ResponseBody(Generic[T]):
|
|
||||||
"""The generic response body for API return."""
|
|
||||||
|
|
||||||
success: bool
|
|
||||||
"""True if this operation is successful, otherwise false."""
|
|
||||||
error: str
|
|
||||||
"""The error message provided when operation failed."""
|
|
||||||
data: T | None
|
|
||||||
"""The payload provided when operation successed."""
|
|
||||||
|
|
||||||
|
|
||||||
class DbException(Exception):
|
|
||||||
"""Error occurs when manipulating with database."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def SafeDatabaseOperation(inner: Callable[P, R]) -> Callable[P, ResponseBody[R]]:
|
|
||||||
def wrapper(*args, **kwargs) -> ResponseBody[R]:
|
|
||||||
# extract self from args
|
|
||||||
self: 'CalendarDatabase' = args[0]
|
|
||||||
# get config
|
|
||||||
cfg = config.get_config()
|
|
||||||
|
|
||||||
with self.mutex:
|
|
||||||
# try to fetching database and allocate database cursor
|
|
||||||
try:
|
|
||||||
db = self._get_db()
|
|
||||||
self._allocate_cursor()
|
|
||||||
except Exception as e:
|
|
||||||
self._free_cursor()
|
|
||||||
if cfg.others.debug:
|
|
||||||
LOGGER.exception(e)
|
|
||||||
return ResponseBody(False, str(e), None)
|
|
||||||
|
|
||||||
# do real data work
|
|
||||||
try:
|
|
||||||
currentTime = utils.GetCurrentTimestamp()
|
|
||||||
if currentTime - self.latestClean > cfg.others.auto_token_clean_duration:
|
|
||||||
self.latestClean = currentTime
|
|
||||||
LOGGER.info('Cleaning outdated token...')
|
|
||||||
self.tokenOper_clean()
|
|
||||||
|
|
||||||
result = ResponseBody(True, '', inner(*args, **kwargs))
|
|
||||||
self._free_cursor()
|
|
||||||
db.commit()
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
self._free_cursor()
|
|
||||||
db.rollback()
|
|
||||||
if cfg.others.debug:
|
|
||||||
LOGGER.exception(e)
|
|
||||||
return ResponseBody(False, str(e), None)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
class CalendarDatabase:
|
|
||||||
|
|
||||||
db: sqlite3.Connection | None
|
|
||||||
cursor: sqlite3.Cursor | None
|
|
||||||
mutex: threading.Lock
|
|
||||||
latestClean: int
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.db = None
|
|
||||||
self.cursor = None
|
|
||||||
self.mutex = threading.Lock()
|
|
||||||
self.latestClean = 0
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
if (self.db is not None):
|
|
||||||
raise DbException('Database is already opened')
|
|
||||||
|
|
||||||
cfg = config.get_config()
|
|
||||||
match cfg.database.driver:
|
|
||||||
case config.DatabaseDriver.SQLITE:
|
|
||||||
self.db = sqlite3.connect(cast(config.SqliteDatabaseConfig, cfg.database.config).path, check_same_thread = False)
|
|
||||||
self.db.execute('PRAGMA encoding = "UTF-8";')
|
|
||||||
self.db.execute('PRAGMA foreign_keys = ON;')
|
|
||||||
case config.DatabaseDriver.MYSQL:
|
|
||||||
raise DbException('Not implemented database')
|
|
||||||
case _:
|
|
||||||
raise DbException('Unknow database type')
|
|
||||||
|
|
||||||
def init(self, username: str, password: str):
|
|
||||||
if (self.db is not None):
|
|
||||||
raise DbException('Database is already opened')
|
|
||||||
|
|
||||||
# establish tables
|
|
||||||
cfg = config.get_config()
|
|
||||||
backend_path = Path(__file__).resolve().parent
|
|
||||||
backend_sql_path = backend_path / 'sql'
|
|
||||||
match cfg.database.driver:
|
|
||||||
case config.DatabaseDriver.SQLITE:
|
|
||||||
sql_file = backend_sql_path / 'sqlite.sql'
|
|
||||||
case config.DatabaseDriver.MYSQL:
|
|
||||||
raise DbException('Not implemented database')
|
|
||||||
case _:
|
|
||||||
raise DbException('Unknow database type')
|
|
||||||
|
|
||||||
self.open()
|
|
||||||
db = self._get_db()
|
|
||||||
|
|
||||||
self._allocate_cursor()
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
|
|
||||||
# execute script for creating tables
|
|
||||||
with open(sql_file, 'r', encoding='utf-8') as fsql:
|
|
||||||
cursor.executescript(fsql.read())
|
|
||||||
# add default user in user table
|
|
||||||
cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', (
|
|
||||||
username,
|
|
||||||
utils.ComputePasswordHash(password),
|
|
||||||
1,
|
|
||||||
utils.GenerateSalt()
|
|
||||||
))
|
|
||||||
|
|
||||||
self._free_cursor()
|
|
||||||
|
|
||||||
# commit to database
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if (self.db is None):
|
|
||||||
LOGGER.warning('Try to close null database.')
|
|
||||||
else:
|
|
||||||
self._free_cursor()
|
|
||||||
self.db.close()
|
|
||||||
self.db = None
|
|
||||||
|
|
||||||
def _get_db(self) -> sqlite3.Connection:
|
|
||||||
if (self.db is None):
|
|
||||||
raise DbException('There is no opened database')
|
|
||||||
else:
|
|
||||||
return self.db
|
|
||||||
|
|
||||||
def _allocate_cursor(self) -> None:
|
|
||||||
if (self.cursor is not None):
|
|
||||||
raise DbException('There is already opened database cursor')
|
|
||||||
else:
|
|
||||||
self.cursor = self._get_db().cursor()
|
|
||||||
|
|
||||||
def _get_cursor(self) -> sqlite3.Cursor:
|
|
||||||
if (self.cursor is None):
|
|
||||||
raise DbException('There is no opened database cursor')
|
|
||||||
else:
|
|
||||||
return self.cursor
|
|
||||||
|
|
||||||
def _free_cursor(self) -> None:
|
|
||||||
if (self.cursor is None):
|
|
||||||
LOGGER.warning('Try to free null databse cursor.')
|
|
||||||
else:
|
|
||||||
self.cursor.close()
|
|
||||||
self.cursor = None
|
|
||||||
|
|
||||||
# ======================= token related internal operation
|
|
||||||
def tokenOper_clean(self):
|
|
||||||
# remove outdated token
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
cursor.execute('DELETE FROM token WHERE [token_expire_on] <= ?',(utils.GetCurrentTimestamp(), ))
|
|
||||||
|
|
||||||
def tokenOper_postpone_expireOn(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
cursor.execute('UPDATE token SET [token_expire_on] = ? WHERE [token] = ?;', (
|
|
||||||
utils.GetTokenExpireOn(),
|
|
||||||
token
|
|
||||||
))
|
|
||||||
|
|
||||||
def tokenOper_check_valid(self, token):
|
|
||||||
self.tokenOper_get_username(token)
|
|
||||||
|
|
||||||
def tokenOper_is_admin(self, username):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
cursor.execute('SELECT [is_admin] FROM user WHERE [name] = ?;',(username, ))
|
|
||||||
cache = cursor.fetchone()[0]
|
|
||||||
return cache == 1
|
|
||||||
|
|
||||||
def tokenOper_get_username(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
cursor.execute('SELECT [user] FROM token WHERE [token] = ? AND [token_expire_on] > ?;',(
|
|
||||||
token,
|
|
||||||
utils.GetCurrentTimestamp()
|
|
||||||
))
|
|
||||||
result = cursor.fetchone()[0]
|
|
||||||
# need postpone expire on time
|
|
||||||
self.tokenOper_postpone_expireOn(token)
|
|
||||||
return result
|
|
||||||
|
|
||||||
# =============================== # =============================== operation function
|
|
||||||
# =============================== common
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def common_salt(self, username):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
salt = utils.GenerateSalt()
|
|
||||||
cursor.execute('UPDATE user SET [salt] = ? WHERE [name] = ?;', (
|
|
||||||
salt,
|
|
||||||
username
|
|
||||||
))
|
|
||||||
return salt
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def common_login(self, username, password, clientUa, clientIp):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
cursor.execute('SELECT [password], [salt] FROM user WHERE [name] = ?;', (username, ))
|
|
||||||
(gotten_salt, gotten_password) = cursor.fetchone()
|
|
||||||
|
|
||||||
if password == utils.ComputePasswordHashWithSalt(gotten_password, gotten_salt):
|
|
||||||
token = utils.GenerateToken(username)
|
|
||||||
cursor.execute('UPDATE user SET [salt] = ? WHERE [name] = ?;', (
|
|
||||||
utils.GenerateSalt(), # regenerate a new slat to prevent re-login try
|
|
||||||
username
|
|
||||||
))
|
|
||||||
cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', (
|
|
||||||
username,
|
|
||||||
token,
|
|
||||||
utils.GetTokenExpireOn(), # add 2 day from now
|
|
||||||
clientUa,
|
|
||||||
clientIp,
|
|
||||||
))
|
|
||||||
return token
|
|
||||||
else:
|
|
||||||
# throw a exception to indicate fail to login
|
|
||||||
raise DbException('Login authentication failed')
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def common_webLogin(self, username, password, clientUa, clientIp):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
LOGGER.debug(f'WebLogin Username: {username}')
|
|
||||||
LOGGER.debug(f'WebLogin Password: {password}')
|
|
||||||
passwordHash = utils.ComputePasswordHash(password)
|
|
||||||
LOGGER.debug(f'WebLogin Password Hash: {passwordHash}')
|
|
||||||
|
|
||||||
cursor.execute('SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;', (username, passwordHash))
|
|
||||||
|
|
||||||
if len(cursor.fetchall()) != 0:
|
|
||||||
token = utils.GenerateToken(username)
|
|
||||||
cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', (
|
|
||||||
username,
|
|
||||||
token,
|
|
||||||
utils.GetTokenExpireOn(), # add 2 day from now
|
|
||||||
clientUa,
|
|
||||||
clientIp,
|
|
||||||
))
|
|
||||||
return token
|
|
||||||
else:
|
|
||||||
# throw a exception to indicate fail to login
|
|
||||||
raise DbException('Login authentication failed')
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def common_logout(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
cursor.execute('DELETE FROM token WHERE [token] = ?;', (token, ))
|
|
||||||
return True
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def common_tokenValid(self, token):
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# =============================== calendar
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def calendar_getFull(self, token, startDateTime, endDateTime):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT calendar.* FROM calendar INNER JOIN collection \
|
|
||||||
ON collection.uuid = calendar.belong_to \
|
|
||||||
WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);',
|
|
||||||
(username, startDateTime, endDateTime))
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def calendar_getList(self, token, startDateTime, endDateTime):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT calendar.uuid FROM calendar INNER JOIN collection \
|
|
||||||
ON collection.uuid = calendar.belong_to \
|
|
||||||
WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);',
|
|
||||||
(username, startDateTime, endDateTime))
|
|
||||||
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def calendar_getDetail(self, token, uuid):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
cursor.execute('SELECT * FROM calendar WHERE [uuid] = ?;', (uuid, ))
|
|
||||||
return cursor.fetchone()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def calendar_update(self, token, uuid, lastChange, **optArgs):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
# get prev data
|
|
||||||
cursor.execute('SELECT * FROM calendar WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange))
|
|
||||||
analyseData = list(cursor.fetchone())
|
|
||||||
|
|
||||||
# construct update data
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
sqlList = [
|
|
||||||
'[last_change] = ?',
|
|
||||||
]
|
|
||||||
argumentsList = [
|
|
||||||
lastupdate,
|
|
||||||
]
|
|
||||||
|
|
||||||
# analyse opt arg
|
|
||||||
reAnalyseLoop = False
|
|
||||||
|
|
||||||
cache = optArgs.get('belongTo', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[belong_to] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
cache = optArgs.get('title', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[title] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
cache = optArgs.get('description', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[description] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
cache = optArgs.get('eventDateTimeStart', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[event_date_time_start] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
reAnalyseLoop = True
|
|
||||||
analyseData[5] = cache
|
|
||||||
cache = optArgs.get('eventDateTimeEnd', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[event_date_time_end] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
cache = optArgs.get('loopRules', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[loop_rules] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
reAnalyseLoop = True
|
|
||||||
analyseData[8] = cache
|
|
||||||
cache = optArgs.get('timezoneOffset', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[timezone_offset] = ?')
|
|
||||||
argumentsList.append(cache)
|
|
||||||
reAnalyseLoop = True
|
|
||||||
analyseData[7] = cache
|
|
||||||
|
|
||||||
if reAnalyseLoop:
|
|
||||||
# re-compute loop data and upload it into list
|
|
||||||
sqlList.append('[loop_date_time_start] = ?')
|
|
||||||
argumentsList.append(analyseData[5])
|
|
||||||
sqlList.append('[loop_date_time_end] = ?')
|
|
||||||
argumentsList.append(str(dt.ResolveLoopStr(
|
|
||||||
analyseData[8],
|
|
||||||
analyseData[5],
|
|
||||||
analyseData[7]
|
|
||||||
)))
|
|
||||||
|
|
||||||
# execute
|
|
||||||
argumentsList.append(uuid)
|
|
||||||
cursor.execute('UPDATE calendar SET {} WHERE [uuid] = ?;'.format(', '.join(sqlList)),
|
|
||||||
tuple(argumentsList))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to update due to no matched rows or too much rows.')
|
|
||||||
return lastupdate
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def calendar_add(self, token, belongTo, title, description, eventDateTimeStart, eventDateTimeEnd, loopRules, timezoneOffset):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
newuuid = utils.GenerateUUID()
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
|
|
||||||
# analyse loopRules and output following 2 fileds.
|
|
||||||
loopDateTimeStart = eventDateTimeStart
|
|
||||||
loopDateTimeEnd = dt.ResolveLoopStr(loopRules, eventDateTimeStart, timezoneOffset)
|
|
||||||
|
|
||||||
cursor.execute('INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);',
|
|
||||||
(newuuid,
|
|
||||||
belongTo,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
lastupdate,
|
|
||||||
eventDateTimeStart,
|
|
||||||
eventDateTimeEnd,
|
|
||||||
timezoneOffset,
|
|
||||||
loopRules,
|
|
||||||
loopDateTimeStart,
|
|
||||||
loopDateTimeEnd))
|
|
||||||
return newuuid
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def calendar_delete(self, token, uuid, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
cursor.execute('DELETE FROM calendar WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
# =============================== collection
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_getFullOwn(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ?;', (username, ))
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_getListOwn(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT [uuid] FROM collection WHERE [user] = ?;', (username, ))
|
|
||||||
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_getDetailOwn(self, token, uuid):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ? AND [uuid] = ?;', (username, uuid))
|
|
||||||
return cursor.fetchone()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_addOwn(self, token, newname):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
newuuid = utils.GenerateUUID()
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
cursor.execute('INSERT INTO collection VALUES (?, ?, ?, ?);',
|
|
||||||
(newuuid, newname, username, lastupdate))
|
|
||||||
return newuuid
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_updateOwn(self, token, uuid, newname, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
cursor.execute('UPDATE collection SET [name] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (
|
|
||||||
newname,
|
|
||||||
lastupdate,
|
|
||||||
uuid,
|
|
||||||
lastChange
|
|
||||||
))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to update due to no matched rows or too much rows.')
|
|
||||||
return lastupdate
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_deleteOwn(self, token, uuid, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
cursor.execute('DELETE FROM collection WHERE [uuid] = ? AND [last_change] = ?;', (
|
|
||||||
uuid,
|
|
||||||
lastChange
|
|
||||||
))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_getSharing(self, token, uuid):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
cursor.execute('SELECT [target] FROM share WHERE [uuid] = ?;', (uuid, ))
|
|
||||||
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_deleteSharing(self, token, uuid, target, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
cursor.execute('UPDATE collection SET [last_change] = ?, WHERE [uuid] = ? AND [last_change] = ?;', (lastupdate, uuid, lastChange))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
|
|
||||||
cursor.execute('DELETE FROM share WHERE [uuid] = ? AND [target] = ?;', (uuid, target))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
|
|
||||||
return lastupdate
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_addSharing(self, token, uuid, target, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
cursor.execute('UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (lastupdate, uuid, lastChange))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM share WHERE [uuid] = ? AND [target] = ?;', (uuid, target))
|
|
||||||
if len(cursor.fetchall()) != 0:
|
|
||||||
raise DbException('Fail to insert duplicated item.')
|
|
||||||
cursor.execute('INSERT INTO share VALUES (?, ?);', (uuid, target))
|
|
||||||
|
|
||||||
return lastupdate
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def collection_getShared(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT collection.uuid, collection.name, collection.user \
|
|
||||||
FROM share INNER JOIN collection \
|
|
||||||
ON share.uuid = collection.uuid \
|
|
||||||
WHERE share.target = ?;', (username, ))
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
# =============================== todo
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def todo_getFull(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT * FROM todo WHERE [belong_to] = ?;', (username, ))
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def todo_getList(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT [uuid] FROM todo WHERE [belong_to] = ?;', (username, ))
|
|
||||||
return tuple(map(lambda x: x[0], cursor.fetchall()))
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def todo_getDetail(self, token, uuid):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('SELECT * FROM todo WHERE [belong_to] = ? AND [uuid] = ?;', (username, uuid))
|
|
||||||
return cursor.fetchone()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def todo_add(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
newuuid = utils.GenerateUUID()
|
|
||||||
lastupdate = utils.GenerateUUID()
|
|
||||||
returnedData = (
|
|
||||||
newuuid,
|
|
||||||
username,
|
|
||||||
'',
|
|
||||||
lastupdate,
|
|
||||||
)
|
|
||||||
cursor.execute('INSERT INTO todo VALUES (?, ?, ?, ?);', returnedData)
|
|
||||||
return returnedData
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def todo_update(self, token, uuid, data, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
# check valid token
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
# update
|
|
||||||
newLastChange = utils.GenerateUUID()
|
|
||||||
cursor.execute('UPDATE todo SET [data] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (
|
|
||||||
data,
|
|
||||||
newLastChange,
|
|
||||||
uuid,
|
|
||||||
lastChange
|
|
||||||
))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to update due to no matched rows or too much rows.')
|
|
||||||
return newLastChange
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def todo_delete(self, token, uuid, lastChange):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
# check valid token
|
|
||||||
self.tokenOper_check_valid(token)
|
|
||||||
|
|
||||||
# delete
|
|
||||||
cursor.execute('DELETE FROM todo WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# =============================== admin
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def admin_get(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
if not self.tokenOper_is_admin(username):
|
|
||||||
raise DbException('Permission denied.')
|
|
||||||
|
|
||||||
cursor.execute('SELECT [name], [is_admin] FROM user;')
|
|
||||||
return tuple(map(lambda x: (x[0], x[1] == 1), cursor.fetchall()))
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def admin_add(self, token, newname):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
if not self.tokenOper_is_admin(username):
|
|
||||||
raise DbException('Permission denied.')
|
|
||||||
|
|
||||||
newpassword = utils.ComputePasswordHash(utils.GenerateUUID())
|
|
||||||
cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', (
|
|
||||||
newname,
|
|
||||||
newpassword,
|
|
||||||
0,
|
|
||||||
utils.GenerateSalt()
|
|
||||||
))
|
|
||||||
return (newname, False)
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def admin_update(self, token, _username, **optArgs):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
if not self.tokenOper_is_admin(username):
|
|
||||||
raise DbException('Permission denied.')
|
|
||||||
|
|
||||||
# construct data
|
|
||||||
sqlList = []
|
|
||||||
argumentsList = []
|
|
||||||
|
|
||||||
# analyse opt arg
|
|
||||||
cache = optArgs.get('password', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[password] = ?')
|
|
||||||
argumentsList.append(utils.ComputePasswordHash(cache))
|
|
||||||
cache = optArgs.get('isAdmin', None)
|
|
||||||
if cache is not None:
|
|
||||||
sqlList.append('[is_admin] = ?')
|
|
||||||
argumentsList.append(1 if cache else 0)
|
|
||||||
|
|
||||||
# execute
|
|
||||||
argumentsList.append(_username)
|
|
||||||
cursor.execute('UPDATE user SET {} WHERE [name] = ?;'.format(', '.join(sqlList)),
|
|
||||||
tuple(argumentsList))
|
|
||||||
LOGGER.debug(cache)
|
|
||||||
LOGGER.debug(tuple(argumentsList))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to update due to no matched rows or too much rows.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def admin_delete(self, token, username):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
_username = self.tokenOper_get_username(token)
|
|
||||||
if not self.tokenOper_is_admin(_username):
|
|
||||||
raise DbException('Permission denied.')
|
|
||||||
|
|
||||||
# delete
|
|
||||||
cursor.execute('DELETE FROM user WHERE [name] = ?;', (username, ))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
# =============================== profile
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def profile_isAdmin(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
return self.tokenOper_is_admin(username)
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def profile_changePassword(self, token, newpassword):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
cursor.execute('UPDATE user SET [password] = ? WHERE [name] = ?;', (
|
|
||||||
utils.ComputePasswordHash(newpassword),
|
|
||||||
username
|
|
||||||
))
|
|
||||||
return True
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def profile_getToken(self, token):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
username = self.tokenOper_get_username(token)
|
|
||||||
|
|
||||||
cursor.execute('SELECT * FROM token WHERE [user] = ?;', (
|
|
||||||
username,
|
|
||||||
))
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
@SafeDatabaseOperation
|
|
||||||
def profile_deleteToken(self, token, deleteToken):
|
|
||||||
cursor = self._get_cursor()
|
|
||||||
_username = self.tokenOper_get_username(token)
|
|
||||||
|
|
||||||
# delete
|
|
||||||
cursor.execute('DELETE FROM token WHERE [user] = ? AND [token] = ?;', (
|
|
||||||
_username,
|
|
||||||
deleteToken
|
|
||||||
))
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise DbException('Fail to delete due to no matched rows or too much rows.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
import typing
|
|
||||||
from functools import reduce
|
|
||||||
import utils
|
|
||||||
|
|
||||||
MonthDayCount = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
|
||||||
|
|
||||||
MIN_DATETIME = datetime.datetime(1950, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc)
|
|
||||||
MAX_DATETIME = datetime.datetime(2200, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc)
|
|
||||||
MIN_TIMESTAMP = int(MIN_DATETIME.timestamp() / 60)
|
|
||||||
MAX_TIMESTAMP = int(MAX_DATETIME.timestamp() / 60)
|
|
||||||
DAY1_SPAN = 60 * 24
|
|
||||||
DAY7_SPAN = 7 * DAY1_SPAN
|
|
||||||
|
|
||||||
LoopHandle = typing.Callable[[re.Match, int, int, int], int]
|
|
||||||
|
|
||||||
def ResolveLoopStr(strl: str, starttime: int, tzoffset: int) -> int:
|
|
||||||
# check no loop
|
|
||||||
if strl == '':
|
|
||||||
return starttime
|
|
||||||
|
|
||||||
# try compute from loopStop
|
|
||||||
(loopRules, loopStopRules) = strl.split('-')
|
|
||||||
cache = precompiledLoopStopRules['infinity'].search(loopStopRules)
|
|
||||||
if cache is not None:
|
|
||||||
return MAX_TIMESTAMP
|
|
||||||
cache = precompiledLoopStopRules['datetime'].search(loopStopRules)
|
|
||||||
if cache is not None:
|
|
||||||
return int(cache.group(1)) # group 1 is datetime
|
|
||||||
cache = precompiledLoopStopRules['times'].search(loopStopRules)
|
|
||||||
if cache is not None:
|
|
||||||
loopTimes = int(cache.group(1)) # for follwing calc
|
|
||||||
else:
|
|
||||||
raise Exception('Invalid loopStopRules') # invalid rules
|
|
||||||
|
|
||||||
for rules in precompiledLoopRules:
|
|
||||||
cache = rules[0].search(loopRules)
|
|
||||||
if cache is not None:
|
|
||||||
return rules[1](cache, starttime, loopTimes, tzoffset)
|
|
||||||
else:
|
|
||||||
raise Exception('Invalid loopRules')
|
|
||||||
|
|
||||||
|
|
||||||
def LoopHandle_Year(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
|
||||||
clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset))
|
|
||||||
isStrict = searchResult.group(1) == 'S'
|
|
||||||
yearSpan = int(searchResult.group(2))
|
|
||||||
|
|
||||||
times -= 1
|
|
||||||
newYear = clientYear = clientDate.year
|
|
||||||
newMonth = clientMonth = clientDate.month
|
|
||||||
newDay = clientDay = clientDate.day
|
|
||||||
if clientMonth == 2 and clientDay == 29:
|
|
||||||
if isStrict:
|
|
||||||
realSpan = utils.LCM(yearSpan, 4)
|
|
||||||
logging.debug(realSpan)
|
|
||||||
valCache = starttime
|
|
||||||
while valCache < MAX_TIMESTAMP and times > 0:
|
|
||||||
newYear += realSpan
|
|
||||||
if not IsLeapYear(newYear):
|
|
||||||
continue
|
|
||||||
valCache = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
|
||||||
times -= 1
|
|
||||||
else:
|
|
||||||
newYear += times * yearSpan
|
|
||||||
if not IsLeapYear(newYear):
|
|
||||||
newDay = 28 # migrate to 28
|
|
||||||
else:
|
|
||||||
# if times == 1, no extra datetime need to be added
|
|
||||||
newYear += times * yearSpan
|
|
||||||
|
|
||||||
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
|
||||||
|
|
||||||
def LoopHandle_Month(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
|
||||||
isStrict = searchResult.group(1) == 'S'
|
|
||||||
loopType = searchResult.group(2)
|
|
||||||
monthSpan = int(searchResult.group(3))
|
|
||||||
|
|
||||||
# we should get original data in each method
|
|
||||||
times -= 1
|
|
||||||
clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset))
|
|
||||||
newYear = clientYear = clientDate.year
|
|
||||||
newMonth = clientMonth = clientDate.month
|
|
||||||
newDay = clientDay = clientDate.day
|
|
||||||
# data struct
|
|
||||||
# dayStatistics =
|
|
||||||
# (dayForwards || dayBackwards || weeksForward, dayOfWeek || weeksBackwards, dayOfWeek)
|
|
||||||
# ( A || B || C || D )
|
|
||||||
dayStatistics = GetDayInMonth(clientYear, clientMonth, clientDay)
|
|
||||||
|
|
||||||
if isStrict:
|
|
||||||
if loopType == 'A':
|
|
||||||
while times > 0:
|
|
||||||
newMonth += monthSpan
|
|
||||||
if newMonth > 12:
|
|
||||||
newYear += int((newMonth - 1) / 12)
|
|
||||||
newMonth = ((newMonth - 1) % 12) + 1
|
|
||||||
if newYear > MAX_DATETIME.year:
|
|
||||||
break
|
|
||||||
maxDays = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0)
|
|
||||||
if dayStatistics[0] <= maxDays:
|
|
||||||
times -= 1
|
|
||||||
elif loopType == 'B':
|
|
||||||
while times > 0:
|
|
||||||
newMonth += monthSpan
|
|
||||||
if newMonth > 12:
|
|
||||||
newYear += int((newMonth - 1) / 12)
|
|
||||||
newMonth = ((newMonth - 1) % 12) + 1
|
|
||||||
if newYear > MAX_DATETIME.year:
|
|
||||||
break
|
|
||||||
maxDays = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0)
|
|
||||||
if dayStatistics[1] <= maxDays:
|
|
||||||
times -= 1
|
|
||||||
elif loopType == 'C':
|
|
||||||
while times > 0:
|
|
||||||
newMonth += monthSpan
|
|
||||||
if newMonth > 12:
|
|
||||||
newYear += int((newMonth - 1) / 12)
|
|
||||||
newMonth = ((newMonth - 1) % 12) + 1
|
|
||||||
if newYear > MAX_DATETIME.year:
|
|
||||||
break
|
|
||||||
monthStatistics = GetMonthWeekStatistics(newYear, newMonth)
|
|
||||||
if dayStatistics[2] <= monthStatistics[dayStatistics[3]]:
|
|
||||||
times -= 1
|
|
||||||
elif loopType == 'D':
|
|
||||||
while times > 0:
|
|
||||||
newMonth += monthSpan
|
|
||||||
if newMonth > 12:
|
|
||||||
newYear += int((newMonth - 1) / 12)
|
|
||||||
newMonth = ((newMonth - 1) % 12) + 1
|
|
||||||
if newYear > MAX_DATETIME.year:
|
|
||||||
break
|
|
||||||
monthStatistics = GetMonthWeekStatistics(newYear, newMonth)
|
|
||||||
if dayStatistics[4] <= monthStatistics[dayStatistics[5]]:
|
|
||||||
times -= 1
|
|
||||||
else:
|
|
||||||
newMonth += times * monthSpan
|
|
||||||
newYear += int((newMonth - 1) / 12)
|
|
||||||
newMonth = ((newMonth - 1) % 12) + 1
|
|
||||||
|
|
||||||
# all method need calc newDay and it should be the last day of current selected month
|
|
||||||
# so calc it in there
|
|
||||||
newDay = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0)
|
|
||||||
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
|
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
|
||||||
|
|
||||||
def LoopHandle_Week(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
|
||||||
weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1)))
|
|
||||||
weekEventCount = reduce(lambda x, y: x + (1 if y else 0), weekOccupied, 0)
|
|
||||||
if weekEventCount == 0:
|
|
||||||
raise Exception('Invalid week format')
|
|
||||||
|
|
||||||
weekSpan = int(searchResult.group(2))
|
|
||||||
nowDayOfWeek = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset)).weekday()
|
|
||||||
if not weekOccupied[nowDayOfWeek]:
|
|
||||||
times-=1 # if first event is not suit for week loop rules, minus one more event to suit it.
|
|
||||||
fullWeek = int(times / weekEventCount)
|
|
||||||
remainEvent = times % weekEventCount
|
|
||||||
|
|
||||||
val = starttime + DAY7_SPAN * fullWeek * weekSpan
|
|
||||||
if val > MAX_TIMESTAMP:
|
|
||||||
return MAX_TIMESTAMP # return now, to reduce calc usage
|
|
||||||
|
|
||||||
while remainEvent != 0:
|
|
||||||
val += DAY1_SPAN
|
|
||||||
if weekOccupied[nowDayOfWeek % 7]:
|
|
||||||
remainEvent -= 1
|
|
||||||
nowDayOfWeek += 1
|
|
||||||
|
|
||||||
val -= 1
|
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
|
||||||
|
|
||||||
def LoopHandle_Day(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
|
|
||||||
val = starttime + DAY1_SPAN * times * int(searchResult.group(1))
|
|
||||||
val -= 1
|
|
||||||
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
|
|
||||||
|
|
||||||
precompiledLoopRules: tuple[tuple[re.Pattern, LoopHandle], ...] = (
|
|
||||||
(re.compile(r'^Y([SR]{1})([1-9]\d*)$'), LoopHandle_Year),
|
|
||||||
(re.compile(r'^M([SR]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month),
|
|
||||||
(re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week),
|
|
||||||
(re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day)
|
|
||||||
)
|
|
||||||
|
|
||||||
precompiledLoopStopRules: dict[str, re.Pattern] = {
|
|
||||||
'infinity': re.compile(r'^F$'),
|
|
||||||
'datetime': re.compile(r'^D([1-9]\d*|0)$'),
|
|
||||||
'times': re.compile(r'^T([1-9]\d*)$')
|
|
||||||
}
|
|
||||||
|
|
||||||
def LeapYearCountEx(endYear: int, includeThis: bool = False, baseYear: int = 1, includeBase: bool = True):
|
|
||||||
if not includeThis:
|
|
||||||
endYear -= 1
|
|
||||||
if includeBase:
|
|
||||||
baseYear -= 1
|
|
||||||
|
|
||||||
endly = int(endYear / 4)
|
|
||||||
endly -= int(endYear / 100)
|
|
||||||
endly += int(endYear / 400)
|
|
||||||
|
|
||||||
basely = int(baseYear / 4)
|
|
||||||
basely -= int(baseYear / 100)
|
|
||||||
basely += int(baseYear / 400)
|
|
||||||
|
|
||||||
return (endly - basely)
|
|
||||||
|
|
||||||
def LeapYearCount(year: int):
|
|
||||||
return LeapYearCountEx(year, False, 1, True)
|
|
||||||
|
|
||||||
def IsLeapYear(year: int):
|
|
||||||
isLeap = False
|
|
||||||
if year % 4 == 0:
|
|
||||||
isLeap = True
|
|
||||||
if year % 100 == 0:
|
|
||||||
isLeap = False
|
|
||||||
if year % 400 == 0:
|
|
||||||
isLeap = True
|
|
||||||
return isLeap
|
|
||||||
|
|
||||||
def DaysCount(year: int, month: int, day: int):
|
|
||||||
ly = LeapYearCountEx(year, False, 1, True)
|
|
||||||
days = 365 * (year - 1)
|
|
||||||
days += ly
|
|
||||||
|
|
||||||
for index in range(1, month, 1):
|
|
||||||
days += MonthDayCount[index - 1]
|
|
||||||
|
|
||||||
if (month > 2) and IsLeapYear(year):
|
|
||||||
days += 1
|
|
||||||
|
|
||||||
days += day - 1
|
|
||||||
return days
|
|
||||||
|
|
||||||
def DayOfWeek(year: int, month: int, day: int):
|
|
||||||
# as we know, 1/1/1900 is Monday.
|
|
||||||
# via this method, we can got 1/1/1 is Monday
|
|
||||||
# compute day span
|
|
||||||
days=DaysCount(year, month, day)
|
|
||||||
|
|
||||||
# return day of week (from 0 - 6, corresponding with python)
|
|
||||||
return days % 7
|
|
||||||
|
|
||||||
def GetDayInMonth(year: int, month: int, day: int):
|
|
||||||
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
|
|
||||||
firstDayOfWeek = DayOfWeek(year, month, 1)
|
|
||||||
dayOfWeek = (firstDayOfWeek + day - 1) % 7
|
|
||||||
|
|
||||||
dayForwards = day
|
|
||||||
dayBackwards = days - day + 1
|
|
||||||
|
|
||||||
weeksForward = int((dayForwards - 1) / 7) + 1
|
|
||||||
weeksBackwards = int((dayBackwards - 1) / 7) + 1
|
|
||||||
|
|
||||||
return (dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek)
|
|
||||||
|
|
||||||
def GetMonthWeekStatistics(year: int, month: int):
|
|
||||||
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
|
|
||||||
firstDayOfWeek = DayOfWeek(year, month, 1)
|
|
||||||
lastDayOfWeek = (firstDayOfWeek + days - 1) % 7
|
|
||||||
|
|
||||||
result = [4, 4, 4, 4, 4, 4, 4]
|
|
||||||
remain = days % 7
|
|
||||||
week = firstDayOfWeek
|
|
||||||
while remain > 0:
|
|
||||||
result[week % 7] += 1
|
|
||||||
week += 1
|
|
||||||
remain -= 1
|
|
||||||
|
|
||||||
return tuple(result)
|
|
||||||
|
|
||||||
class UTCTimezone(datetime.tzinfo):
|
|
||||||
|
|
||||||
__offset: int
|
|
||||||
|
|
||||||
def __init__(self, offset: int = 0):
|
|
||||||
self.__offset = offset
|
|
||||||
|
|
||||||
def utcoffset(self, dt):
|
|
||||||
return datetime.timedelta(minutes=self.__offset)
|
|
||||||
|
|
||||||
def tzname(self, dt):
|
|
||||||
return 'UTC {}'.format(self.__offset)
|
|
||||||
|
|
||||||
def dst(self, dt):
|
|
||||||
return datetime.timedelta(0)
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import logging
|
|
||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
def _build_logger() -> tuple[logging.Logger, logging.Handler]:
|
|
||||||
# Create a new logger which is independent with Flask
|
|
||||||
logger = logging.getLogger("my_console_logger")
|
|
||||||
# Avoid message was propagated to root logger or captured by Flask logger.
|
|
||||||
logger.propagate = False
|
|
||||||
# Set initial level.
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Create StreamHandler to output into stderr.
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.DEBUG)
|
|
||||||
# Set format for it.
|
|
||||||
formatter = logging.Formatter("[%(levelname)s] %(message)s")
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
# Add handler
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
return (logger, console_handler)
|
|
||||||
|
|
||||||
|
|
||||||
(LOGGER, CONSOLE_HANDLER) = _build_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class LoggerLevel(enum.IntEnum):
|
|
||||||
DEBUG = enum.auto()
|
|
||||||
INFO = enum.auto()
|
|
||||||
|
|
||||||
|
|
||||||
def set_level(level: LoggerLevel) -> None:
|
|
||||||
logging_level: int = logging.INFO
|
|
||||||
match level:
|
|
||||||
case LoggerLevel.DEBUG:
|
|
||||||
logging_level = logging.DEBUG
|
|
||||||
case LoggerLevel.INFO:
|
|
||||||
logging_level = logging.INFO
|
|
||||||
|
|
||||||
LOGGER.setLevel(logging_level)
|
|
||||||
CONSOLE_HANDLER.setLevel(logging_level)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "coleaf-backend"
|
|
||||||
version = "1.1.0"
|
|
||||||
description = "The backend of coconut-leaf."
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
dependencies = [
|
|
||||||
"flask==2.2.3",
|
|
||||||
]
|
|
||||||
[tool.uv]
|
|
||||||
constraint-dependencies = [
|
|
||||||
"Werkzeug==2.2.2",
|
|
||||||
"MarkupSafe==2.1.5"
|
|
||||||
]
|
|
||||||
@@ -1,513 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
from flask import request
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Callable, ParamSpec, TypeVar, Generic
|
|
||||||
|
|
||||||
import config
|
|
||||||
import database
|
|
||||||
import utils
|
|
||||||
from logger import LOGGER
|
|
||||||
from database import ResponseBody
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
calendar_db = database.CalendarDatabase()
|
|
||||||
|
|
||||||
# region: API Route
|
|
||||||
|
|
||||||
# region: Common
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/common/salt", methods=["POST"])
|
|
||||||
def api_common_saltHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.common_salt, (FormField("username", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/common/login", methods=["POST"])
|
|
||||||
def api_common_loginHandle():
|
|
||||||
clientInfo = FetchClientNetworkInfo()
|
|
||||||
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.common_login,
|
|
||||||
(
|
|
||||||
FormField("username", str, False),
|
|
||||||
FormField("password", str, False),
|
|
||||||
FormField("clientUa", str, False),
|
|
||||||
FormField("clientIp", str, False),
|
|
||||||
),
|
|
||||||
{"clientUa": clientInfo.user_agent, "clientIp": clientInfo.ip_addr},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/common/webLogin", methods=["POST"])
|
|
||||||
def api_common_webLoginHandle():
|
|
||||||
clientInfo = FetchClientNetworkInfo()
|
|
||||||
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.common_webLogin,
|
|
||||||
(
|
|
||||||
FormField("username", str, False),
|
|
||||||
FormField("password", str, False),
|
|
||||||
FormField("clientUa", str, False),
|
|
||||||
FormField("clientIp", str, False),
|
|
||||||
),
|
|
||||||
{"clientUa": clientInfo.user_agent, "clientIp": clientInfo.ip_addr},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/common/logout", methods=["POST"])
|
|
||||||
def api_common_logoutHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.common_logout, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/common/tokenValid", methods=["POST"])
|
|
||||||
def api_common_tokenValidHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.common_tokenValid, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region: Calendar
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar/getFull", methods=["POST"])
|
|
||||||
def api_calendar_getFullHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.calendar_getFull,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("startDateTime", int, False),
|
|
||||||
FormField("endDateTime", int, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar/getList", methods=["POST"])
|
|
||||||
def api_calendar_getListHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.calendar_getList,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("startDateTime", int, False),
|
|
||||||
FormField("endDateTime", int, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar/getDetail", methods=["POST"])
|
|
||||||
def api_calendar_getDetailHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.calendar_getDetail,
|
|
||||||
(FormField("token", str, False), FormField("uuid", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar/update", methods=["POST"])
|
|
||||||
def api_calendar_updateHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.calendar_update,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("belongTo", str, True),
|
|
||||||
FormField("title", str, True),
|
|
||||||
FormField("description", str, True),
|
|
||||||
FormField("eventDateTimeStart", int, True),
|
|
||||||
FormField("eventDateTimeEnd", int, True),
|
|
||||||
FormField("loopRules", str, True),
|
|
||||||
FormField("timezoneOffset", int, True),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar/add", methods=["POST"])
|
|
||||||
def api_calendar_addHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.calendar_add,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("belongTo", str, False),
|
|
||||||
FormField("title", str, False),
|
|
||||||
FormField("description", str, False),
|
|
||||||
FormField("eventDateTimeStart", int, False),
|
|
||||||
FormField("eventDateTimeEnd", int, False),
|
|
||||||
FormField("loopRules", str, False),
|
|
||||||
FormField("timezoneOffset", int, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar/delete", methods=["POST"])
|
|
||||||
def api_calendar_deleteHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.calendar_delete,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region: Collection
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/getFullOwn", methods=["POST"])
|
|
||||||
def api_collection_getFullOwnHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_getFullOwn, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/getListOwn", methods=["POST"])
|
|
||||||
def api_collection_getListOwnHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_getListOwn, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/getDetailOwn", methods=["POST"])
|
|
||||||
def api_collection_getDetailOwnHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_getDetailOwn,
|
|
||||||
(FormField("token", str, False), FormField("uuid", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/addOwn", methods=["POST"])
|
|
||||||
def api_collection_addOwnHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_addOwn,
|
|
||||||
(FormField("token", str, False), FormField("name", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/updateOwn", methods=["POST"])
|
|
||||||
def api_collection_updateOwnHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_updateOwn,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("name", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/deleteOwn", methods=["POST"])
|
|
||||||
def api_collection_deleteOwnHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_deleteOwn,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/getSharing", methods=["POST"])
|
|
||||||
def api_collection_getSharingHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_getSharing,
|
|
||||||
(FormField("token", str, False), FormField("uuid", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/deleteSharing", methods=["POST"])
|
|
||||||
def api_collection_deleteSharingHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_deleteSharing,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("target", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/addSharing", methods=["POST"])
|
|
||||||
def api_collection_addSharingHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_addSharing,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("target", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/collection/getShared", methods=["POST"])
|
|
||||||
def api_collection_getSharedHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.collection_getShared, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region: Todo
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/getFull", methods=["POST"])
|
|
||||||
def api_todo_getFullHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.todo_getFull, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/getList", methods=["POST"])
|
|
||||||
def api_todo_getListHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.todo_getList, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/getDetail", methods=["POST"])
|
|
||||||
def api_todo_getDetailHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.todo_getDetail,
|
|
||||||
(FormField("token", str, False), FormField("uuid", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/add", methods=["POST"])
|
|
||||||
def api_todo_addHandle():
|
|
||||||
return SmartDbCaller(calendar_db.todo_add, (FormField("token", str, False),), None)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/update", methods=["POST"])
|
|
||||||
def api_todo_updateHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.todo_update,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("data", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/delete", methods=["POST"])
|
|
||||||
def api_todo_deleteHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.todo_delete,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("uuid", str, False),
|
|
||||||
FormField("lastChange", str, False),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region: Admin
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/get", methods=["POST"])
|
|
||||||
def api_admin_getHandle():
|
|
||||||
return SmartDbCaller(calendar_db.admin_get, (FormField("token", str, False),), None)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/add", methods=["POST"])
|
|
||||||
def api_admin_addHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.admin_add,
|
|
||||||
(FormField("token", str, False), FormField("username", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/update", methods=["POST"])
|
|
||||||
def api_admin_updateHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.admin_update,
|
|
||||||
(
|
|
||||||
FormField("token", str, False),
|
|
||||||
FormField("username", str, False),
|
|
||||||
FormField("password", str, True),
|
|
||||||
FormField("isAdmin", utils.Str2Bool, True),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/delete", methods=["POST"])
|
|
||||||
def api_admin_deleteHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.admin_delete,
|
|
||||||
(FormField("token", str, False), FormField("username", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region: Profile
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/profile/isAdmin", methods=["POST"])
|
|
||||||
def api_profile_isAdminHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.profile_isAdmin, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/profile/changePassword", methods=["POST"])
|
|
||||||
def api_profile_changePasswordHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.profile_changePassword,
|
|
||||||
(FormField("token", str, False), FormField("password", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/profile/getToken", methods=["POST"])
|
|
||||||
def api_profile_getTokenHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.profile_getToken, (FormField("token", str, False),), None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/profile/deleteToken", methods=["POST"])
|
|
||||||
def api_profile_deleteTokenHandle():
|
|
||||||
return SmartDbCaller(
|
|
||||||
calendar_db.profile_deleteToken,
|
|
||||||
(FormField("token", str, False), FormField("deleteToken", str, False)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region: Utilities
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ClientNetworkInfo:
|
|
||||||
user_agent: str
|
|
||||||
"""The user agent of client."""
|
|
||||||
ip_addr: str
|
|
||||||
"""The IP address of client."""
|
|
||||||
|
|
||||||
|
|
||||||
def FetchClientNetworkInfo() -> ClientNetworkInfo:
|
|
||||||
clientUa = request.user_agent.string
|
|
||||||
forwardIpList = request.headers.getlist("X-Forwarded-For")
|
|
||||||
if forwardIpList:
|
|
||||||
clientIp = forwardIpList[0]
|
|
||||||
else:
|
|
||||||
directIp = request.remote_addr
|
|
||||||
if directIp is not None:
|
|
||||||
clientIp = directIp
|
|
||||||
else:
|
|
||||||
clientIp = "0.0.0.0"
|
|
||||||
|
|
||||||
return ClientNetworkInfo(clientUa, clientIp)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class FormField:
|
|
||||||
name: str
|
|
||||||
"""The name of form field."""
|
|
||||||
ty: Callable[[str], Any]
|
|
||||||
"""The type of form field."""
|
|
||||||
is_optional: bool
|
|
||||||
"""True if this form field is optional, otherwise false."""
|
|
||||||
|
|
||||||
|
|
||||||
def SmartDbCaller(
|
|
||||||
db_method: Callable[..., ResponseBody[Any]],
|
|
||||||
fields: tuple[FormField, ...],
|
|
||||||
padding_form: dict[str, str] | None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
opt_param_counter = 0
|
|
||||||
lost_required: bool = False
|
|
||||||
param_list: list[Any] = []
|
|
||||||
opt_param_dict: dict[str, Any] = {}
|
|
||||||
|
|
||||||
# fetch user passed form
|
|
||||||
user_form: dict[str, str] = request.form.to_dict()
|
|
||||||
LOGGER.debug(f"User Form: {user_form}")
|
|
||||||
# overwrite user form by our padding form
|
|
||||||
if padding_form is not None:
|
|
||||||
user_form.update(padding_form)
|
|
||||||
LOGGER.debug(f"Padded User Form: {user_form}")
|
|
||||||
|
|
||||||
# check fields one by one
|
|
||||||
for field in fields:
|
|
||||||
value = user_form.get(field.name, None)
|
|
||||||
if value is not None:
|
|
||||||
value = field.ty(value)
|
|
||||||
|
|
||||||
if field.is_optional:
|
|
||||||
# optional param
|
|
||||||
if value is not None:
|
|
||||||
opt_param_dict[field.name] = value
|
|
||||||
opt_param_counter += 1
|
|
||||||
else:
|
|
||||||
# required param
|
|
||||||
if value is None:
|
|
||||||
lost_required = True
|
|
||||||
else:
|
|
||||||
param_list.append(value)
|
|
||||||
|
|
||||||
# Only execute database function if there is no lost required fields.
|
|
||||||
# And fulfill one of following requirements:
|
|
||||||
# 1. There are all required fields (optional parameter count is zero).
|
|
||||||
# 1. Or, there is some optional parameter.
|
|
||||||
LOGGER.debug(f"Has Lost Required Parameter: {lost_required}")
|
|
||||||
LOGGER.debug(f"All Optional Parameter Count: {opt_param_counter}")
|
|
||||||
LOGGER.debug(f"Available Optional Parameter Count: {len(opt_param_dict)}")
|
|
||||||
result: ResponseBody[Any]
|
|
||||||
if lost_required == False and (opt_param_counter == 0 or len(opt_param_dict) != 0):
|
|
||||||
result = db_method(*param_list, **opt_param_dict)
|
|
||||||
else:
|
|
||||||
result = ResponseBody(False, "Invalid parameter", None)
|
|
||||||
|
|
||||||
return ConstructResponseBody(result)
|
|
||||||
|
|
||||||
|
|
||||||
def ConstructResponseBody(body: ResponseBody[Any]) -> dict[str, Any]:
|
|
||||||
return {"success": body.success, "error": body.error, "data": body.data}
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
calendar_db.open()
|
|
||||||
app.run(port=config.get_config().web.port)
|
|
||||||
calendar_db.close()
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
CREATE TABLE user(
|
|
||||||
[name] TEXT NOT NULL,
|
|
||||||
[password] TEXT NOT NULL,
|
|
||||||
[is_admin] TINYINT NOT NULL CHECK(is_admin = 1 OR is_admin = 0),
|
|
||||||
[salt] INTEGER NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE token(
|
|
||||||
[user] TEXT NOT NULL,
|
|
||||||
[token] TEXT UNIQUE NOT NULL,
|
|
||||||
[token_expire_on] BIGINT NOT NULL,
|
|
||||||
[ua] TEXT NOT NULL,
|
|
||||||
[ip] TEXT NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE collection(
|
|
||||||
[uuid] TEXT NOT NULL,
|
|
||||||
[name] TEXT NOT NULL,
|
|
||||||
[user] TEXT NOT NULL,
|
|
||||||
[last_change] TEXT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (uuid),
|
|
||||||
FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE share(
|
|
||||||
[uuid] TEXT NOT NULL,
|
|
||||||
[target] TEXT NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (uuid) REFERENCES collection(uuid) ON DELETE CASCADE
|
|
||||||
FOREIGN KEY (target) REFERENCES user(name) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE calendar(
|
|
||||||
[uuid] TEXT NOT NULL,
|
|
||||||
[belong_to] TEXT NOT NULL,
|
|
||||||
|
|
||||||
[title] TEXT NOT NULL,
|
|
||||||
[description] TEXT NOT NULL,
|
|
||||||
[last_change] TEXT NOT NULL,
|
|
||||||
|
|
||||||
[event_date_time_start] BIGINT NOT NULL,
|
|
||||||
[event_date_time_end] BIGINT NOT NULL,
|
|
||||||
[timezone_offset] INT NOT NULL,
|
|
||||||
|
|
||||||
[loop_rules] TEXT NOT NULL,
|
|
||||||
[loop_date_time_start] BIGINT NOT NULL,
|
|
||||||
[loop_date_time_end] BIGINT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (uuid),
|
|
||||||
FOREIGN KEY (belong_to) REFERENCES collection(uuid) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE todo(
|
|
||||||
[uuid] TEXT NOT NULL,
|
|
||||||
[belong_to] TEXT NOT NULL,
|
|
||||||
|
|
||||||
[data] TEXT NOT NULL,
|
|
||||||
[last_change] TEXT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (uuid),
|
|
||||||
FOREIGN KEY (belong_to) REFERENCES user(name) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import random
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import math
|
|
||||||
import re
|
|
||||||
|
|
||||||
USERNAME_PATTERN: re.Pattern = re.compile("^[0-9A-Za-z]+$")
|
|
||||||
PASSWORD_PATTERN: re.Pattern = re.compile("^[!-~]+$")
|
|
||||||
|
|
||||||
|
|
||||||
def IsValidUsername(strl: str) -> bool:
|
|
||||||
return USERNAME_PATTERN.match(strl) is not None
|
|
||||||
|
|
||||||
|
|
||||||
def IsValidPassword(strl: str) -> bool:
|
|
||||||
return PASSWORD_PATTERN.match(strl) is not None
|
|
||||||
|
|
||||||
|
|
||||||
def ComputePasswordHash(password: str) -> str:
|
|
||||||
s = hashlib.sha256()
|
|
||||||
s.update(password.encode("utf-8"))
|
|
||||||
return s.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def GenerateUUID() -> str:
|
|
||||||
return str(uuid.uuid1())
|
|
||||||
|
|
||||||
|
|
||||||
def GenerateToken(username: str) -> str:
|
|
||||||
s = hashlib.sha256()
|
|
||||||
s.update(username.encode("utf-8"))
|
|
||||||
s.update(GenerateUUID().encode("utf-8"))
|
|
||||||
return s.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def GenerateSalt() -> int:
|
|
||||||
return random.randint(0, 6172748)
|
|
||||||
|
|
||||||
|
|
||||||
def ComputePasswordHashWithSalt(passwordHashed: str, salt: int) -> str:
|
|
||||||
s = hashlib.sha256()
|
|
||||||
s.update((passwordHashed + str(salt)).encode("utf-8"))
|
|
||||||
return s.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def GetCurrentTimestamp() -> int:
|
|
||||||
return int(time.time())
|
|
||||||
|
|
||||||
|
|
||||||
def GetTokenExpireOn() -> int:
|
|
||||||
return GetCurrentTimestamp() + 60 * 60 * 24 * 2 # add 2 day from now
|
|
||||||
|
|
||||||
|
|
||||||
def Str2Bool(strl: str) -> bool:
|
|
||||||
return strl.lower() == "true"
|
|
||||||
|
|
||||||
|
|
||||||
def GCD(a: int, b: int) -> int:
|
|
||||||
return math.gcd(a, b)
|
|
||||||
|
|
||||||
|
|
||||||
def LCM(a: int, b: int) -> int:
|
|
||||||
return (a * b) // GCD(a, b)
|
|
||||||
Generated
-117
@@ -1,117 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 2
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
|
|
||||||
[manifest]
|
|
||||||
constraints = [
|
|
||||||
{ name = "markupsafe", specifier = "==2.1.5" },
|
|
||||||
{ name = "werkzeug", specifier = "==2.2.2" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.3.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "coleaf-backend"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "flask" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [{ name = "flask", specifier = "==2.2.3" }]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flask"
|
|
||||||
version = "2.2.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "itsdangerous" },
|
|
||||||
{ name = "jinja2" },
|
|
||||||
{ name = "werkzeug" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/5c/ff9047989bd995b1098d14b03013f160225db2282925b517bb4a967752ee/Flask-2.2.3.tar.gz", hash = "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", size = 697599, upload-time = "2023-02-15T22:43:57.265Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl", hash = "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d", size = 101839, upload-time = "2023-02-15T22:43:55.501Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itsdangerous"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jinja2"
|
|
||||||
version = "3.1.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markupsafe"
|
|
||||||
version = "2.1.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "werkzeug"
|
|
||||||
version = "2.2.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/c1/1c8e539f040acd80f844c69a5ef8e2fccdf8b442dabb969e497b55d544e1/Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", size = 844378, upload-time = "2022-08-08T21:44:15.376Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/27/be6ddbcf60115305205de79c29004a0c6bc53cec814f733467b1bb89386d/Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5", size = 232700, upload-time = "2022-08-08T21:44:13.251Z" },
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# coconut-leaf Backend
|
||||||
|
|
||||||
|
The backend service of coconut-leaf,
|
||||||
|
implemented with [Go](https://go.dev/) + [Gin](https://gin-gonic.com/),
|
||||||
|
migrated from the old Python Flask backend.
|
||||||
|
The data layer executes SQL directly (no ORM).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Currently only SQLite is fully implemented.
|
||||||
|
> MySQL interfaces are placeholders (all endpoints return "not implemented" errors).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Go >= 1.26**
|
||||||
|
- **C compiler (cgo)**: The SQLite driver [`mattn/go-sqlite3`](https://github.com/mattn/go-sqlite3) requires CGO feature.
|
||||||
|
- Windows: MSYS2's gcc is recommended.
|
||||||
|
- Linux / macOS: System-provided gcc / clang works.
|
||||||
|
- Ensure `CGO_ENABLED=1` before building (usually enabled by default when a C compiler is available).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
In the `backend/` directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build -o coconut-leaf
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The first build compiles SQLite C source code; 1–3 minutes is normal.
|
||||||
|
>
|
||||||
|
> On Windows, executables built with MSYS2 depend on MSYS2 runtime libraries
|
||||||
|
> (I use MSYS2 UCRT64, so it may include `libgcc_s_*`, `libwinpthread-1.dll`, etc.).
|
||||||
|
> Either add the DLL directory to PATH or copy those DLLs next to the executable.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Uses TOML format, specified via `-config` parameter.
|
||||||
|
See the full template at [coconut-leaf.template.toml](../assets/coconut-leaf.template.toml) in the `assets` directory.
|
||||||
|
|
||||||
|
## Command-line Arguments
|
||||||
|
|
||||||
|
| Argument | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `-config <PATH>` | **Required**, path to the TOML configuration file |
|
||||||
|
| `-init` | Initialize the system: create tables and the first admin user |
|
||||||
|
| `-username <NAME>` | Used with `-init`, the initial admin username |
|
||||||
|
| `-password <PASS>` | Used with `-init`, the initial admin password |
|
||||||
|
|
||||||
|
`-username` / `-password` are required only when `-init` is specified and are validated
|
||||||
|
(username: `[0-9A-Za-z]+`, password: all visible ASCII characters, i.e., `[!-~]+`).
|
||||||
|
|
||||||
|
## Initialization and Runtime
|
||||||
|
|
||||||
|
### First Deployment
|
||||||
|
|
||||||
|
First deployment includes creating tables and the admin user, then running as usual:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./coconut-leaf --config coconut-leaf.toml --init --username admin --password "your-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Normal Runtime
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./coconut-leaf --config coconut-leaf.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
After starting, it listens on `web.port`.
|
||||||
|
Debug logs are written to stderr in the format `[<time>] [<level>] <message> [key=value ...]`.
|
||||||
+11
-3
@@ -13,10 +13,14 @@ type CLIArgs struct {
|
|||||||
Config string
|
Config string
|
||||||
// Init indicates whether to initialize the calendar system before running.
|
// Init indicates whether to initialize the calendar system before running.
|
||||||
Init bool
|
Init bool
|
||||||
|
// Username is the first admin user's name; required together with -init.
|
||||||
|
Username string
|
||||||
|
// Password is the first admin user's password; required together with -init.
|
||||||
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses the command-line arguments via the standard flag package and
|
// Parse parses the command-line arguments via the standard flag package and
|
||||||
// returns a *CLIArgs. When the required --config flag is missing, it prints the
|
// returns a *CLIArgs. When the required -config flag is missing, it prints the
|
||||||
// usage and exits with status 2 (matching argparse's exit code).
|
// usage and exits with status 2 (matching argparse's exit code).
|
||||||
func Parse() *CLIArgs {
|
func Parse() *CLIArgs {
|
||||||
args := &CLIArgs{}
|
args := &CLIArgs{}
|
||||||
@@ -30,15 +34,19 @@ func Parse() *CLIArgs {
|
|||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --config maps to the legacy -c/--config (required), --init to -i/--init.
|
// -config maps to the legacy -c/--config (required), -init to -i/--init.
|
||||||
|
// -username/-password replace the legacy interactive GetUsernamePassword
|
||||||
|
// prompt and are required together with --init.
|
||||||
flag.StringVar(&args.Config, "config", "", "The configuration file `CONFIG_TOML` for coconut-leaf")
|
flag.StringVar(&args.Config, "config", "", "The configuration file `CONFIG_TOML` for coconut-leaf")
|
||||||
flag.BoolVar(&args.Init, "init", false, "Set for initialize the calendar system")
|
flag.BoolVar(&args.Init, "init", false, "Set for initialize the calendar system")
|
||||||
|
flag.StringVar(&args.Username, "username", "", "The first admin user's name (required with -init)")
|
||||||
|
flag.StringVar(&args.Password, "password", "", "The first admin user's password (required with -init)")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// The standard flag package has no built-in "required", so validate manually.
|
// The standard flag package has no built-in "required", so validate manually.
|
||||||
if args.Config == "" {
|
if args.Config == "" {
|
||||||
fmt.Fprintln(os.Stderr, "error: the required flag --config is not provided")
|
fmt.Fprintln(os.Stderr, "error: the required flag -config is not provided")
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CalendarUpdateOptions carries the optional fields of CalendarUpdate. A nil
|
||||||
|
// pointer means "not provided", mirroring the legacy **optArgs semantics: a
|
||||||
|
// field is applied only when its pointer is non-nil.
|
||||||
|
type CalendarUpdateOptions struct {
|
||||||
|
BelongTo *string
|
||||||
|
Title *string
|
||||||
|
Description *string
|
||||||
|
EventDateTimeStart *int64
|
||||||
|
EventDateTimeEnd *int64
|
||||||
|
LoopRules *string
|
||||||
|
TimezoneOffset *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUpdateOptions carries the optional fields of AdminUpdate. A nil pointer
|
||||||
|
// means "not provided".
|
||||||
|
type AdminUpdateOptions struct {
|
||||||
|
Password *string
|
||||||
|
IsAdmin *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deps bundles the shared dependencies a database implementation needs: the
|
||||||
|
// loaded configuration and a logger. It is passed to database constructors and
|
||||||
|
// stored inside each implementation so they can read config and emit logs.
|
||||||
|
type Deps struct {
|
||||||
|
Cfg *config.Config
|
||||||
|
Logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database describes every database operation the Gin server may use. It is the
|
||||||
|
// Go equivalent of the legacy CalendarDatabase public surface: each method takes
|
||||||
|
// a context.Context for cancellation and returns (any, error). The server layer
|
||||||
|
// is responsible for wrapping the result into an HTTP response body.
|
||||||
|
type Database interface {
|
||||||
|
Init(username, password string) error
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
CommonSalt(ctx context.Context, username string) (any, error)
|
||||||
|
CommonLogin(ctx context.Context, username, password, clientUa, clientIp string) (any, error)
|
||||||
|
CommonWebLogin(ctx context.Context, username, password, clientUa, clientIp string) (any, error)
|
||||||
|
CommonLogout(ctx context.Context, token string) (any, error)
|
||||||
|
CommonTokenValid(ctx context.Context, token string) (any, error)
|
||||||
|
|
||||||
|
CalendarGetFull(ctx context.Context, token string, startDateTime, endDateTime int64) (any, error)
|
||||||
|
CalendarGetList(ctx context.Context, token string, startDateTime, endDateTime int64) (any, error)
|
||||||
|
CalendarGetDetail(ctx context.Context, token, uuid string) (any, error)
|
||||||
|
CalendarUpdate(ctx context.Context, token, uuid, lastChange string, opts CalendarUpdateOptions) (any, error)
|
||||||
|
CalendarAdd(ctx context.Context, token, belongTo, title, description string, eventDateTimeStart, eventDateTimeEnd int64, loopRules string, timezoneOffset int64) (any, error)
|
||||||
|
CalendarDelete(ctx context.Context, token, uuid, lastChange string) (any, error)
|
||||||
|
|
||||||
|
CollectionGetFullOwn(ctx context.Context, token string) (any, error)
|
||||||
|
CollectionGetListOwn(ctx context.Context, token string) (any, error)
|
||||||
|
CollectionGetDetailOwn(ctx context.Context, token, uuid string) (any, error)
|
||||||
|
CollectionAddOwn(ctx context.Context, token, name string) (any, error)
|
||||||
|
CollectionUpdateOwn(ctx context.Context, token, uuid, name, lastChange string) (any, error)
|
||||||
|
CollectionDeleteOwn(ctx context.Context, token, uuid, lastChange string) (any, error)
|
||||||
|
CollectionGetSharing(ctx context.Context, token, uuid string) (any, error)
|
||||||
|
CollectionDeleteSharing(ctx context.Context, token, uuid, target, lastChange string) (any, error)
|
||||||
|
CollectionAddSharing(ctx context.Context, token, uuid, target, lastChange string) (any, error)
|
||||||
|
CollectionGetShared(ctx context.Context, token string) (any, error)
|
||||||
|
|
||||||
|
TodoGetFull(ctx context.Context, token string) (any, error)
|
||||||
|
TodoGetList(ctx context.Context, token string) (any, error)
|
||||||
|
TodoGetDetail(ctx context.Context, token, uuid string) (any, error)
|
||||||
|
TodoAdd(ctx context.Context, token string) (any, error)
|
||||||
|
TodoUpdate(ctx context.Context, token, uuid, data, lastChange string) (any, error)
|
||||||
|
TodoDelete(ctx context.Context, token, uuid, lastChange string) (any, error)
|
||||||
|
|
||||||
|
AdminGet(ctx context.Context, token string) (any, error)
|
||||||
|
AdminAdd(ctx context.Context, token, username string) (any, error)
|
||||||
|
AdminUpdate(ctx context.Context, token, username string, opts AdminUpdateOptions) (any, error)
|
||||||
|
AdminDelete(ctx context.Context, token, username string) (any, error)
|
||||||
|
|
||||||
|
ProfileIsAdmin(ctx context.Context, token string) (any, error)
|
||||||
|
ProfileChangePassword(ctx context.Context, token, password string) (any, error)
|
||||||
|
ProfileGetToken(ctx context.Context, token string) (any, error)
|
||||||
|
ProfileDeleteToken(ctx context.Context, token, deleteToken string) (any, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errNotImplemented = errors.New("mysql database: not implemented")
|
||||||
|
|
||||||
|
// stubDatabase provides a not-implemented default for every Database method.
|
||||||
|
// MysqlDatabase embeds it so the interface is satisfied without hand-writing
|
||||||
|
// each method; individual methods can be overridden on MysqlDatabase later.
|
||||||
|
type stubDatabase struct{}
|
||||||
|
|
||||||
|
func (stubDatabase) Init(username, password string) error { return errNotImplemented }
|
||||||
|
func (stubDatabase) Close() error { return errNotImplemented }
|
||||||
|
|
||||||
|
func (stubDatabase) CommonSalt(ctx context.Context, username string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CommonLogin(ctx context.Context, username, password, clientUa, clientIp string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CommonWebLogin(ctx context.Context, username, password, clientUa, clientIp string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CommonLogout(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CommonTokenValid(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubDatabase) CalendarGetFull(ctx context.Context, token string, startDateTime, endDateTime int64) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CalendarGetList(ctx context.Context, token string, startDateTime, endDateTime int64) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CalendarGetDetail(ctx context.Context, token, uuid string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CalendarUpdate(ctx context.Context, token, uuid, lastChange string, opts CalendarUpdateOptions) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CalendarAdd(ctx context.Context, token, belongTo, title, description string, eventDateTimeStart, eventDateTimeEnd int64, loopRules string, timezoneOffset int64) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CalendarDelete(ctx context.Context, token, uuid, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubDatabase) CollectionGetFullOwn(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionGetListOwn(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionGetDetailOwn(ctx context.Context, token, uuid string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionAddOwn(ctx context.Context, token, name string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionUpdateOwn(ctx context.Context, token, uuid, name, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionDeleteOwn(ctx context.Context, token, uuid, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionGetSharing(ctx context.Context, token, uuid string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionDeleteSharing(ctx context.Context, token, uuid, target, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionAddSharing(ctx context.Context, token, uuid, target, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) CollectionGetShared(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubDatabase) TodoGetFull(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) TodoGetList(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) TodoGetDetail(ctx context.Context, token, uuid string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) TodoAdd(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) TodoUpdate(ctx context.Context, token, uuid, data, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) TodoDelete(ctx context.Context, token, uuid, lastChange string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubDatabase) AdminGet(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) AdminAdd(ctx context.Context, token, username string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) AdminUpdate(ctx context.Context, token, username string, opts AdminUpdateOptions) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) AdminDelete(ctx context.Context, token, username string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubDatabase) ProfileIsAdmin(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) ProfileChangePassword(ctx context.Context, token, password string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) ProfileGetToken(ctx context.Context, token string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
func (stubDatabase) ProfileDeleteToken(ctx context.Context, token, deleteToken string) (any, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// MysqlDatabase implements Database as a fully not-implemented stub by
|
||||||
|
// embedding stubDatabase. Real MySQL support can be filled in later by
|
||||||
|
// overriding individual methods.
|
||||||
|
type MysqlDatabase struct {
|
||||||
|
stubDatabase
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMysqlDatabase returns a not-implemented MysqlDatabase. The deps are stored
|
||||||
|
// for symmetry with NewSqlite3Database but not yet used.
|
||||||
|
func NewMysqlDatabase(deps Deps) (*MysqlDatabase, error) {
|
||||||
|
return &MysqlDatabase{deps: deps}, nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,444 @@
|
|||||||
|
package dt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// All time values in this file use minute-granularity timestamps (UNIX seconds
|
||||||
|
// / 60), mirroring the legacy dt.py. tzoffset is also in minutes.
|
||||||
|
|
||||||
|
var monthDayCount = [12]int64{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
|
||||||
|
|
||||||
|
var (
|
||||||
|
minDatetime = time.Date(1950, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
maxDatetime = time.Date(2200, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
minTimestamp = minDatetime.Unix() / 60
|
||||||
|
maxTimestamp = maxDatetime.Unix() / 60
|
||||||
|
maxDatetimeYear = int64(maxDatetime.Year())
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
day1Span = int64(60 * 24)
|
||||||
|
day7Span = int64(7) * day1Span
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loopYearRule = regexp.MustCompile(`^Y([SR]{1})([1-9][0-9]*)$`)
|
||||||
|
loopMonthRule = regexp.MustCompile(`^M([SR]{1})([ABCD])([1-9][0-9]*)$`)
|
||||||
|
loopWeekRule = regexp.MustCompile(`^W([TF]{7})([1-9][0-9]*)$`)
|
||||||
|
loopDayRule = regexp.MustCompile(`^D([1-9][0-9]*)$`)
|
||||||
|
|
||||||
|
loopStopInfinity = regexp.MustCompile(`^F$`)
|
||||||
|
loopStopDatetime = regexp.MustCompile(`^D([1-9][0-9]*|0)$`)
|
||||||
|
loopStopTimes = regexp.MustCompile(`^T([1-9][0-9]*)$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type loopHandler func(sub []string, starttime, loopTimes, tzoffset int64) (int64, error)
|
||||||
|
|
||||||
|
var loopRules = []struct {
|
||||||
|
re *regexp.Regexp
|
||||||
|
handler loopHandler
|
||||||
|
}{
|
||||||
|
{loopYearRule, loopHandleYear},
|
||||||
|
{loopMonthRule, loopHandleMonth},
|
||||||
|
{loopWeekRule, loopHandleWeek},
|
||||||
|
{loopDayRule, loopHandleDay},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveLoopStr parses a loop-rule string of the form "[rules]-[stop]" and
|
||||||
|
// returns the loop end timestamp (minute granularity), given the start time and
|
||||||
|
// timezone offset (both in minutes). Mirrors the legacy dt.ResolveLoopStr.
|
||||||
|
func ResolveLoopStr(strl string, starttime, tzoffset int64) (int64, error) {
|
||||||
|
// check no loop
|
||||||
|
if strl == "" {
|
||||||
|
return starttime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(strl, "-")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0, errors.New("invalid loop rule: expected exactly one '-' separator")
|
||||||
|
}
|
||||||
|
rulesStr, stopStr := parts[0], parts[1]
|
||||||
|
|
||||||
|
// try compute from loopStop
|
||||||
|
if loopStopInfinity.MatchString(stopStr) {
|
||||||
|
return maxTimestamp, nil
|
||||||
|
}
|
||||||
|
if m := loopStopDatetime.FindStringSubmatch(stopStr); m != nil {
|
||||||
|
ts, err := strconv.ParseInt(m[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid loop datetime stop: %w", err)
|
||||||
|
}
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var loopTimes int64
|
||||||
|
if m := loopStopTimes.FindStringSubmatch(stopStr); m != nil {
|
||||||
|
t, err := strconv.ParseInt(m[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid loop times stop: %w", err)
|
||||||
|
}
|
||||||
|
loopTimes = t
|
||||||
|
} else {
|
||||||
|
return 0, errors.New("invalid loop stop rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range loopRules {
|
||||||
|
if m := rule.re.FindStringSubmatch(rulesStr); m != nil {
|
||||||
|
return rule.handler(m, starttime, loopTimes, tzoffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("invalid loop rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientDateComponents extracts the (year, month, day) wall-clock components of
|
||||||
|
// starttime (minute timestamp) under the fixed offset tzoffset (minutes). This
|
||||||
|
// mirrors python's datetime.fromtimestamp(starttime*60, UTCTimezone(tzoffset)).
|
||||||
|
// Note time.FixedZone takes seconds, hence tzoffset*60.
|
||||||
|
func clientDateComponents(starttime int64, tzoffset int64) (int64, int64, int64) {
|
||||||
|
loc := time.FixedZone("offset", int(tzoffset*60))
|
||||||
|
t := time.Unix(starttime*60, 0).In(loc)
|
||||||
|
return int64(t.Year()), int64(t.Month()), int64(t.Day())
|
||||||
|
}
|
||||||
|
|
||||||
|
func loopHandleYear(sub []string, starttime, times, tzoffset int64) (int64, error) {
|
||||||
|
clientYear, clientMonth, clientDay := clientDateComponents(starttime, tzoffset)
|
||||||
|
isStrict := sub[1] == "S"
|
||||||
|
yearSpan, err := strconv.ParseInt(sub[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid year span: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
times--
|
||||||
|
newYear, newMonth, newDay := clientYear, clientMonth, clientDay
|
||||||
|
if clientMonth == 2 && clientDay == 29 {
|
||||||
|
if isStrict {
|
||||||
|
realSpan := utils.LCM(yearSpan, 4)
|
||||||
|
valCache := starttime
|
||||||
|
for valCache < maxTimestamp && times > 0 {
|
||||||
|
newYear += realSpan
|
||||||
|
if !isLeapYear(newYear) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valCache = starttime + day1Span*(daysCount(newYear, newMonth, newDay)-daysCount(clientYear, clientMonth, clientDay))
|
||||||
|
times--
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newYear += times * yearSpan
|
||||||
|
if !isLeapYear(newYear) {
|
||||||
|
newDay = 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if times == 1, no extra datetime need to be added
|
||||||
|
newYear += times * yearSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
val := starttime + day1Span*(daysCount(newYear, newMonth, newDay)-daysCount(clientYear, clientMonth, clientDay))
|
||||||
|
if val < maxTimestamp {
|
||||||
|
return val, nil
|
||||||
|
} else {
|
||||||
|
return maxTimestamp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loopHandleMonth(sub []string, starttime, times, tzoffset int64) (int64, error) {
|
||||||
|
isStrict := sub[1] == "S"
|
||||||
|
loopType := sub[2]
|
||||||
|
monthSpan, err := strconv.ParseInt(sub[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid month span: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we should get original data in each method
|
||||||
|
times--
|
||||||
|
clientYear, clientMonth, clientDay := clientDateComponents(starttime, tzoffset)
|
||||||
|
newYear, newMonth, newDay := clientYear, clientMonth, clientDay
|
||||||
|
// data struct
|
||||||
|
// ds =
|
||||||
|
// (dayForwards || dayBackwards || weeksForward, dayOfWeek || weeksBackwards, dayOfWeek)
|
||||||
|
// ( A || B || C || D )
|
||||||
|
ds := getDayInMonth(clientYear, clientMonth, clientDay)
|
||||||
|
|
||||||
|
advanceMonth := func() {
|
||||||
|
newMonth += monthSpan
|
||||||
|
if newMonth > 12 {
|
||||||
|
newYear += (newMonth - 1) / 12
|
||||||
|
newMonth = ((newMonth - 1) % 12) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStrict {
|
||||||
|
switch loopType {
|
||||||
|
case "A":
|
||||||
|
for times > 0 {
|
||||||
|
advanceMonth()
|
||||||
|
if newYear > maxDatetimeYear {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
maxDays := monthDayCount[newMonth-1]
|
||||||
|
if newMonth == 2 && isLeapYear(newYear) {
|
||||||
|
maxDays++
|
||||||
|
}
|
||||||
|
if ds.daysForward <= maxDays {
|
||||||
|
times--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "B":
|
||||||
|
for times > 0 {
|
||||||
|
advanceMonth()
|
||||||
|
if newYear > maxDatetimeYear {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
maxDays := monthDayCount[newMonth-1]
|
||||||
|
if newMonth == 2 && isLeapYear(newYear) {
|
||||||
|
maxDays++
|
||||||
|
}
|
||||||
|
if ds.daysBackward <= maxDays {
|
||||||
|
times--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "C":
|
||||||
|
for times > 0 {
|
||||||
|
advanceMonth()
|
||||||
|
if newYear > maxDatetimeYear {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ms := getMonthWeekStatistics(newYear, newMonth)
|
||||||
|
if ds.weeksForward <= ms[ds.weeksForwardDayOfWeek] {
|
||||||
|
times--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "D":
|
||||||
|
for times > 0 {
|
||||||
|
advanceMonth()
|
||||||
|
if newYear > maxDatetimeYear {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ms := getMonthWeekStatistics(newYear, newMonth)
|
||||||
|
if ds.weeksBackward <= ms[ds.weeksBackwardDayOfWeek] {
|
||||||
|
times--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newMonth += times * monthSpan
|
||||||
|
newYear += (newMonth - 1) / 12
|
||||||
|
newMonth = ((newMonth - 1) % 12) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// all method need calc newDay and it should be the last day of current selected month
|
||||||
|
// so calc it in there
|
||||||
|
newDay = monthDayCount[newMonth-1]
|
||||||
|
if newMonth == 2 && isLeapYear(newYear) {
|
||||||
|
newDay++
|
||||||
|
}
|
||||||
|
val := starttime + day1Span*(daysCount(newYear, newMonth, newDay)-daysCount(clientYear, clientMonth, clientDay))
|
||||||
|
if val < maxTimestamp {
|
||||||
|
return val, nil
|
||||||
|
} else {
|
||||||
|
return maxTimestamp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loopHandleWeek(sub []string, starttime, times, tzoffset int64) (int64, error) {
|
||||||
|
weekStr := sub[1]
|
||||||
|
var weekOccupied [7]bool
|
||||||
|
var weekEventCount int64
|
||||||
|
for i := range 7 {
|
||||||
|
weekOccupied[i] = weekStr[i] == 'T'
|
||||||
|
if weekOccupied[i] {
|
||||||
|
weekEventCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if weekEventCount == 0 {
|
||||||
|
return 0, errors.New("invalid week format")
|
||||||
|
}
|
||||||
|
|
||||||
|
weekSpan, err := strconv.ParseInt(sub[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid week span: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the ported dayOfWeek (Monday=0) instead of Go's time.Weekday (Sunday=0)
|
||||||
|
// so the week rule's Monday-first indexing stays consistent.
|
||||||
|
cy, cm, cd := clientDateComponents(starttime, tzoffset)
|
||||||
|
nowDayOfWeek := dayOfWeek(cy, cm, cd)
|
||||||
|
if !weekOccupied[nowDayOfWeek] {
|
||||||
|
// if first event is not suit for week loop rules, minus one more event to suit it.
|
||||||
|
times--
|
||||||
|
}
|
||||||
|
fullWeek := times / weekEventCount
|
||||||
|
remainEvent := times % weekEventCount
|
||||||
|
|
||||||
|
val := starttime + day7Span*fullWeek*weekSpan
|
||||||
|
if val > maxTimestamp {
|
||||||
|
// return now, to reduce calc usage
|
||||||
|
return maxTimestamp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for remainEvent != 0 {
|
||||||
|
val += day1Span
|
||||||
|
if weekOccupied[nowDayOfWeek%7] {
|
||||||
|
remainEvent--
|
||||||
|
}
|
||||||
|
nowDayOfWeek++
|
||||||
|
}
|
||||||
|
|
||||||
|
val--
|
||||||
|
if val < maxTimestamp {
|
||||||
|
return val, nil
|
||||||
|
} else {
|
||||||
|
return maxTimestamp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loopHandleDay(sub []string, starttime, times, tzoffset int64) (int64, error) {
|
||||||
|
span, err := strconv.ParseInt(sub[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid day span: %w", err)
|
||||||
|
}
|
||||||
|
val := starttime + day1Span*times*span - 1
|
||||||
|
if val < maxTimestamp {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return maxTimestamp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// leapYearCountEx counts leap years following the proleptic Gregorian
|
||||||
|
// 4/100/400 rule. NOTE: the legacy python had a typo in the baseYear branch
|
||||||
|
// (subtracted /400 instead of /100, which cancelled the +/400); it is corrected
|
||||||
|
// here. The fix has no observable effect on current call sites, since baseYear
|
||||||
|
// is always 1 and floor(1/n)=0 for n in {4,100,400}.
|
||||||
|
func leapYearCountEx(endYear int64, includeThis bool, baseYear int64, includeBase bool) int64 {
|
||||||
|
if !includeThis {
|
||||||
|
endYear--
|
||||||
|
}
|
||||||
|
if includeBase {
|
||||||
|
baseYear--
|
||||||
|
}
|
||||||
|
|
||||||
|
endly := endYear / 4
|
||||||
|
endly -= endYear / 100
|
||||||
|
endly += endYear / 400
|
||||||
|
|
||||||
|
basely := baseYear / 4
|
||||||
|
basely -= baseYear / 100
|
||||||
|
basely += baseYear / 400
|
||||||
|
|
||||||
|
return endly - basely
|
||||||
|
}
|
||||||
|
|
||||||
|
func leapYearCount(year int64) int64 {
|
||||||
|
return leapYearCountEx(year, false, 1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLeapYear(year int64) bool {
|
||||||
|
isLeap := false
|
||||||
|
if year%4 == 0 {
|
||||||
|
isLeap = true
|
||||||
|
}
|
||||||
|
if year%100 == 0 {
|
||||||
|
isLeap = false
|
||||||
|
}
|
||||||
|
if year%400 == 0 {
|
||||||
|
isLeap = true
|
||||||
|
}
|
||||||
|
return isLeap
|
||||||
|
}
|
||||||
|
|
||||||
|
func daysCount(year, month, day int64) int64 {
|
||||||
|
ly := leapYearCountEx(year, false, 1, true)
|
||||||
|
days := int64(365) * (year - 1)
|
||||||
|
days += ly
|
||||||
|
|
||||||
|
for index := int64(1); index < month; index++ {
|
||||||
|
days += monthDayCount[index-1]
|
||||||
|
}
|
||||||
|
if month > 2 && isLeapYear(year) {
|
||||||
|
days++
|
||||||
|
}
|
||||||
|
|
||||||
|
days += day - 1
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
// dayOfWeek returns the day of week with Monday=0 .. Sunday=6 (independent of
|
||||||
|
// Go's time.Weekday convention), derived from the portable daysCount.
|
||||||
|
func dayOfWeek(year, month, day int64) int64 {
|
||||||
|
// As we know, Jan 1, 1900 is Monday.
|
||||||
|
// According to this, we can speculate Jan 1, 0001 also is Monday.
|
||||||
|
return daysCount(year, month, day) % 7
|
||||||
|
}
|
||||||
|
|
||||||
|
// dayInMonthInfo holds positional statistics for a day within its month. The
|
||||||
|
// day-of-week values use Monday=0 indexing.
|
||||||
|
type dayInMonthInfo struct {
|
||||||
|
// daysForward is the day count to this day, counting from month head to tail.
|
||||||
|
daysForward int64
|
||||||
|
// daysBackward is the day count to this day, counting from month tail to head.
|
||||||
|
daysBackward int64
|
||||||
|
// weeksForward is the count of the week this day is located in, counting from
|
||||||
|
// month head to tail.
|
||||||
|
weeksForward int64
|
||||||
|
// weeksForwardDayOfWeek is the day-of-week index paired with weeksForward.
|
||||||
|
weeksForwardDayOfWeek int64
|
||||||
|
// weeksBackward is the count of the week this day is located in, counting from
|
||||||
|
// month tail to head.
|
||||||
|
weeksBackward int64
|
||||||
|
// weeksBackwardDayOfWeek is the day-of-week index paired with weeksBackward.
|
||||||
|
weeksBackwardDayOfWeek int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDayInMonth returns the positional statistics of the given date within its
|
||||||
|
// month.
|
||||||
|
func getDayInMonth(year, month, day int64) dayInMonthInfo {
|
||||||
|
days := monthDayCount[month-1]
|
||||||
|
if month == 2 && isLeapYear(year) {
|
||||||
|
days++
|
||||||
|
}
|
||||||
|
firstDayOfWeek := dayOfWeek(year, month, 1)
|
||||||
|
dow := (firstDayOfWeek + day - 1) % 7
|
||||||
|
|
||||||
|
daysForward := day
|
||||||
|
daysBackward := days - day + 1
|
||||||
|
weeksForward := (daysForward-1)/7 + 1
|
||||||
|
weeksBackward := (daysBackward-1)/7 + 1
|
||||||
|
|
||||||
|
return dayInMonthInfo{
|
||||||
|
daysForward: daysForward,
|
||||||
|
daysBackward: daysBackward,
|
||||||
|
weeksForward: weeksForward,
|
||||||
|
weeksForwardDayOfWeek: dow,
|
||||||
|
weeksBackward: weeksBackward,
|
||||||
|
weeksBackwardDayOfWeek: dow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMonthWeekStatistics returns, for each weekday Monday=0..Sunday=6, how many
|
||||||
|
// times that weekday occurs in the given month.
|
||||||
|
func getMonthWeekStatistics(year, month int64) [7]int64 {
|
||||||
|
days := monthDayCount[month-1]
|
||||||
|
if month == 2 && isLeapYear(year) {
|
||||||
|
days++
|
||||||
|
}
|
||||||
|
firstDayOfWeek := dayOfWeek(year, month, 1)
|
||||||
|
// lastDayOfWeek := (firstDayOfWeek + days - 1) % 7
|
||||||
|
|
||||||
|
result := [7]int64{4, 4, 4, 4, 4, 4, 4}
|
||||||
|
remain := days % 7
|
||||||
|
week := firstDayOfWeek
|
||||||
|
for remain > 0 {
|
||||||
|
result[week%7]++
|
||||||
|
week++
|
||||||
|
remain--
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
+20
-19
@@ -8,34 +8,35 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.3 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.48 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.60.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.29.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.57.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.40.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,19 +2,31 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk
|
|||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
||||||
|
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||||
|
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
@@ -25,8 +37,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -38,10 +54,16 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -49,12 +71,16 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||||
|
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -72,21 +98,35 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
|||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho=
|
||||||
|
golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggerLevel expresses the desired logging verbosity. Unlike a full log-level
|
||||||
|
// enum, it only distinguishes between a verbose development mode and a quieter
|
||||||
|
// production mode; it is converted to an slog.Level by New.
|
||||||
|
type LoggerLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Development enables debug-level output, suitable for local development.
|
||||||
|
Development LoggerLevel = iota
|
||||||
|
// Production limits output to info level and above.
|
||||||
|
Production
|
||||||
|
)
|
||||||
|
|
||||||
|
// toSlogLevel converts a LoggerLevel into the corresponding slog.Level,
|
||||||
|
// defaulting to info for any unrecognized value.
|
||||||
|
func toSlogLevel(level LoggerLevel) slog.Level {
|
||||||
|
switch level {
|
||||||
|
case Development:
|
||||||
|
return slog.LevelDebug
|
||||||
|
case Production:
|
||||||
|
return slog.LevelInfo
|
||||||
|
default:
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a *slog.Logger that writes to os.Stderr in the bracketed format
|
||||||
|
// "[<time>] [<LEVEL>] <message> [key=value ...]". Its verbosity is driven by the
|
||||||
|
// given LoggerLevel. This replaces the legacy module-level singleton LOGGER;
|
||||||
|
// callers own the returned logger and thread it through to sub-modules.
|
||||||
|
func New(level LoggerLevel) *slog.Logger {
|
||||||
|
return slog.New(&bracketHandler{w: os.Stderr, level: toSlogLevel(level)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// bracketHandler is a custom slog.Handler emitting the bracketed format
|
||||||
|
// "[<time>] [<LEVEL>] <message>" followed by any attributes as "key=value",
|
||||||
|
// matching the legacy "[LEVEL] message" style with an added timestamp.
|
||||||
|
type bracketHandler struct {
|
||||||
|
w io.Writer
|
||||||
|
level slog.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *bracketHandler) Enabled(_ context.Context, l slog.Level) bool {
|
||||||
|
return l >= h.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *bracketHandler) Handle(_ context.Context, r slog.Record) error {
|
||||||
|
var b []byte
|
||||||
|
b = append(b, '[')
|
||||||
|
b = r.Time.AppendFormat(b, "2006-01-02T15:04:05.000-07:00")
|
||||||
|
b = append(b, "] ["...)
|
||||||
|
b = append(b, r.Level.String()...)
|
||||||
|
b = append(b, "] "...)
|
||||||
|
b = append(b, r.Message...)
|
||||||
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
|
b = append(b, ' ')
|
||||||
|
b = append(b, a.Key...)
|
||||||
|
b = append(b, '=')
|
||||||
|
b = append(b, a.Value.String()...)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
b = append(b, '\n')
|
||||||
|
_, err := h.w.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAttrs and WithGroup are no-ops: this project does not derive sub-loggers
|
||||||
|
// via Logger.With, so they simply return the receiver.
|
||||||
|
func (h *bracketHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
|
||||||
|
|
||||||
|
func (h *bracketHandler) WithGroup(_ string) slog.Handler { return h }
|
||||||
+60
-16
@@ -2,13 +2,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"net/http"
|
"os"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
"github.com/yyc12345/coconut-leaf/backend/cli"
|
"github.com/yyc12345/coconut-leaf/backend/cli"
|
||||||
"github.com/yyc12345/coconut-leaf/backend/config"
|
"github.com/yyc12345/coconut-leaf/backend/config"
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/database"
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/logger"
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/server"
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -16,20 +18,62 @@ func main() {
|
|||||||
|
|
||||||
cfg, err := config.Load(args.Config)
|
cfg, err := config.Load(args.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
fmt.Fprintln(os.Stderr, "error loading config file:", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
_ = cfg
|
|
||||||
|
|
||||||
// 创建默认的 Gin 引擎(包含 Logger 和 Recovery 中间件)
|
// Splash is printed directly; the logger is not built yet.
|
||||||
r := gin.Default()
|
fmt.Println("Coconut-leaf")
|
||||||
|
fmt.Println("A light, self-host and multi-account calendar system")
|
||||||
|
fmt.Println("Project: https://github.com/yyc12345/coconut-leaf")
|
||||||
|
fmt.Println("===================")
|
||||||
|
|
||||||
// 定义路由
|
// Build the logger from the loaded config.
|
||||||
r.GET("/", func(c *gin.Context) {
|
loggerLevel := logger.Production
|
||||||
c.JSON(http.StatusOK, gin.H{
|
if cfg.Others.Debug {
|
||||||
"message": "Hello, Gin!",
|
loggerLevel = logger.Development
|
||||||
})
|
}
|
||||||
})
|
log := logger.New(loggerLevel)
|
||||||
|
|
||||||
//启动服务器(默认端口8088)
|
// Create the database backend selected by the config.
|
||||||
r.Run(":8080")
|
deps := database.Deps{Cfg: cfg, Logger: log}
|
||||||
|
var db database.Database
|
||||||
|
switch cfg.Database.Driver {
|
||||||
|
case config.DatabaseDriverSqlite:
|
||||||
|
db, err = database.NewSqlite3Database(deps)
|
||||||
|
case config.DatabaseDriverMysql:
|
||||||
|
db, err = database.NewMysqlDatabase(deps)
|
||||||
|
default:
|
||||||
|
log.Error("unknown database driver", "driver", cfg.Database.Driver)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to open database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the schema and first admin user when requested.
|
||||||
|
if args.Init {
|
||||||
|
if !utils.IsValidUsername(args.Username) {
|
||||||
|
log.Error("invalid init username")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !utils.IsValidPassword(args.Password) {
|
||||||
|
log.Error("invalid init password")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := db.Init(args.Username, args.Password); err != nil {
|
||||||
|
log.Error("failed to initialize database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve.
|
||||||
|
defer db.Close()
|
||||||
|
log.Info("Starting server...")
|
||||||
|
handler := &server.Handler{DB: db, Logger: log, Cfg: cfg}
|
||||||
|
if err := server.Run(handler); err != nil {
|
||||||
|
log.Error("server stopped with error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,692 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/config"
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/database"
|
||||||
|
"github.com/yyc12345/coconut-leaf/backend/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseBody is the JSON envelope returned by every API endpoint, mirroring
|
||||||
|
// the legacy ConstructResponseBody: {success, error, data}.
|
||||||
|
type ResponseBody struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Data any `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler holds the shared dependencies injected into every Gin handler: the
|
||||||
|
// database, the application logger and the loaded config.
|
||||||
|
type Handler struct {
|
||||||
|
DB database.Database
|
||||||
|
Logger *slog.Logger
|
||||||
|
Cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// region: Utilities
|
||||||
|
|
||||||
|
// respond wraps a (data, error) pair into a ResponseBody. The HTTP status is
|
||||||
|
// always 200; failures are encoded in the body, mirroring the legacy API.
|
||||||
|
func respond(c *gin.Context, data any, err error) {
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, ResponseBody{Success: false, Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, ResponseBody{Success: true, Data: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
// respondInvalidParam replies with the legacy "Invalid parameter" body.
|
||||||
|
func respondInvalidParam(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, ResponseBody{Success: false, Error: "Invalid parameter"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchForm flattens the POST form into a map and logs it at debug level,
|
||||||
|
// mirroring the legacy SmartDbCaller "User Form" log.
|
||||||
|
func fetchForm(c *gin.Context, logger *slog.Logger) map[string]string {
|
||||||
|
_ = c.Request.ParseForm()
|
||||||
|
form := make(map[string]string, len(c.Request.PostForm))
|
||||||
|
for k, v := range c.Request.PostForm {
|
||||||
|
if len(v) > 0 {
|
||||||
|
form[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Debug("User Form", "form", form)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchClientInfo returns the client user agent and IP, mirroring the legacy
|
||||||
|
// FetchClientNetworkInfo. The real IP (X-Forwarded-For behind Nginx) comes from
|
||||||
|
// gin's c.ClientIP().
|
||||||
|
func fetchClientInfo(c *gin.Context) (ua, ip string) {
|
||||||
|
ua = c.Request.UserAgent()
|
||||||
|
ip = c.ClientIP()
|
||||||
|
if ip == "" {
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
}
|
||||||
|
return ua, ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// Run sets up the Gin engine with all routes bound to the handler and starts
|
||||||
|
// listening on the configured web port.
|
||||||
|
func Run(handler *Handler) error {
|
||||||
|
if handler.Cfg.Others.Debug {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
err := r.SetTrustedProxies([]string{
|
||||||
|
"127.0.0.1", // IPv4 本地回环
|
||||||
|
"::1", // IPv6 本地回环
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRoutes(r, handler)
|
||||||
|
return r.Run(fmt.Sprintf(":%d", handler.Cfg.Web.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRoutes(r *gin.Engine, h *Handler) {
|
||||||
|
// common
|
||||||
|
r.POST("/common/salt", h.CommonSalt)
|
||||||
|
r.POST("/common/login", h.CommonLogin)
|
||||||
|
r.POST("/common/webLogin", h.CommonWebLogin)
|
||||||
|
r.POST("/common/logout", h.CommonLogout)
|
||||||
|
r.POST("/common/tokenValid", h.CommonTokenValid)
|
||||||
|
|
||||||
|
// calendar
|
||||||
|
r.POST("/calendar/getFull", h.CalendarGetFull)
|
||||||
|
r.POST("/calendar/getList", h.CalendarGetList)
|
||||||
|
r.POST("/calendar/getDetail", h.CalendarGetDetail)
|
||||||
|
r.POST("/calendar/update", h.CalendarUpdate)
|
||||||
|
r.POST("/calendar/add", h.CalendarAdd)
|
||||||
|
r.POST("/calendar/delete", h.CalendarDelete)
|
||||||
|
|
||||||
|
// collection
|
||||||
|
r.POST("/collection/getFullOwn", h.CollectionGetFullOwn)
|
||||||
|
r.POST("/collection/getListOwn", h.CollectionGetListOwn)
|
||||||
|
r.POST("/collection/getDetailOwn", h.CollectionGetDetailOwn)
|
||||||
|
r.POST("/collection/addOwn", h.CollectionAddOwn)
|
||||||
|
r.POST("/collection/updateOwn", h.CollectionUpdateOwn)
|
||||||
|
r.POST("/collection/deleteOwn", h.CollectionDeleteOwn)
|
||||||
|
r.POST("/collection/getSharing", h.CollectionGetSharing)
|
||||||
|
r.POST("/collection/deleteSharing", h.CollectionDeleteSharing)
|
||||||
|
r.POST("/collection/addSharing", h.CollectionAddSharing)
|
||||||
|
r.POST("/collection/getShared", h.CollectionGetShared)
|
||||||
|
|
||||||
|
// todo
|
||||||
|
r.POST("/todo/getFull", h.TodoGetFull)
|
||||||
|
r.POST("/todo/getList", h.TodoGetList)
|
||||||
|
r.POST("/todo/getDetail", h.TodoGetDetail)
|
||||||
|
r.POST("/todo/add", h.TodoAdd)
|
||||||
|
r.POST("/todo/update", h.TodoUpdate)
|
||||||
|
r.POST("/todo/delete", h.TodoDelete)
|
||||||
|
|
||||||
|
// admin
|
||||||
|
r.POST("/admin/get", h.AdminGet)
|
||||||
|
r.POST("/admin/add", h.AdminAdd)
|
||||||
|
r.POST("/admin/update", h.AdminUpdate)
|
||||||
|
r.POST("/admin/delete", h.AdminDelete)
|
||||||
|
|
||||||
|
// profile
|
||||||
|
r.POST("/profile/isAdmin", h.ProfileIsAdmin)
|
||||||
|
r.POST("/profile/changePassword", h.ProfileChangePassword)
|
||||||
|
r.POST("/profile/getToken", h.ProfileGetToken)
|
||||||
|
r.POST("/profile/deleteToken", h.ProfileDeleteToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// region: API Route
|
||||||
|
|
||||||
|
// region: Common
|
||||||
|
|
||||||
|
func (h *Handler) CommonSalt(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
username, ok := form["username"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CommonSalt(c.Request.Context(), username)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CommonLogin(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
username, okU := form["username"]
|
||||||
|
password, okP := form["password"]
|
||||||
|
if !okU || !okP {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientUa, clientIp := fetchClientInfo(c)
|
||||||
|
data, err := h.DB.CommonLogin(c.Request.Context(), username, password, clientUa, clientIp)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CommonWebLogin(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
username, okU := form["username"]
|
||||||
|
password, okP := form["password"]
|
||||||
|
if !okU || !okP {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientUa, clientIp := fetchClientInfo(c)
|
||||||
|
data, err := h.DB.CommonWebLogin(c.Request.Context(), username, password, clientUa, clientIp)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CommonLogout(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CommonLogout(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CommonTokenValid(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CommonTokenValid(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Calendar
|
||||||
|
|
||||||
|
func (h *Handler) CalendarGetFull(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
sds, okS := form["startDateTime"]
|
||||||
|
eds, okE := form["endDateTime"]
|
||||||
|
if !okT || !okS || !okE {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startDateTime, err1 := strconv.ParseInt(sds, 10, 64)
|
||||||
|
endDateTime, err2 := strconv.ParseInt(eds, 10, 64)
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CalendarGetFull(c.Request.Context(), token, startDateTime, endDateTime)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CalendarGetList(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
sds, okS := form["startDateTime"]
|
||||||
|
eds, okE := form["endDateTime"]
|
||||||
|
if !okT || !okS || !okE {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startDateTime, err1 := strconv.ParseInt(sds, 10, 64)
|
||||||
|
endDateTime, err2 := strconv.ParseInt(eds, 10, 64)
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CalendarGetList(c.Request.Context(), token, startDateTime, endDateTime)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CalendarGetDetail(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CalendarGetDetail(c.Request.Context(), token, uuid)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CalendarUpdate(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts database.CalendarUpdateOptions
|
||||||
|
provided := 0
|
||||||
|
if v, ok := form["belongTo"]; ok {
|
||||||
|
opts.BelongTo = &v
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["title"]; ok {
|
||||||
|
opts.Title = &v
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["description"]; ok {
|
||||||
|
opts.Description = &v
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["eventDateTimeStart"]; ok {
|
||||||
|
n, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.EventDateTimeStart = &n
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["eventDateTimeEnd"]; ok {
|
||||||
|
n, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.EventDateTimeEnd = &n
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["loopRules"]; ok {
|
||||||
|
opts.LoopRules = &v
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["timezoneOffset"]; ok {
|
||||||
|
n, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.TimezoneOffset = &n
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if provided == 0 {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.DB.CalendarUpdate(c.Request.Context(), token, uuid, lastChange, opts)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CalendarAdd(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
belongTo, okB := form["belongTo"]
|
||||||
|
title, okTi := form["title"]
|
||||||
|
description, okD := form["description"]
|
||||||
|
loopRules, okL := form["loopRules"]
|
||||||
|
edtsStr, okES := form["eventDateTimeStart"]
|
||||||
|
edteStr, okEE := form["eventDateTimeEnd"]
|
||||||
|
tzoStr, okTZ := form["timezoneOffset"]
|
||||||
|
if !okT || !okB || !okTi || !okD || !okL || !okES || !okEE || !okTZ {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
edts, err1 := strconv.ParseInt(edtsStr, 10, 64)
|
||||||
|
edte, err2 := strconv.ParseInt(edteStr, 10, 64)
|
||||||
|
tzo, err3 := strconv.ParseInt(tzoStr, 10, 64)
|
||||||
|
if err1 != nil || err2 != nil || err3 != nil {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CalendarAdd(c.Request.Context(), token, belongTo, title, description, edts, edte, loopRules, tzo)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CalendarDelete(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CalendarDelete(c.Request.Context(), token, uuid, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Collection
|
||||||
|
|
||||||
|
func (h *Handler) CollectionGetFullOwn(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionGetFullOwn(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionGetListOwn(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionGetListOwn(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionGetDetailOwn(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionGetDetailOwn(c.Request.Context(), token, uuid)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionAddOwn(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
name, okN := form["name"]
|
||||||
|
if !okT || !okN {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionAddOwn(c.Request.Context(), token, name)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionUpdateOwn(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
name, okN := form["name"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okN || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionUpdateOwn(c.Request.Context(), token, uuid, name, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionDeleteOwn(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionDeleteOwn(c.Request.Context(), token, uuid, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionGetSharing(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionGetSharing(c.Request.Context(), token, uuid)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionDeleteSharing(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
target, okG := form["target"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okG || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionDeleteSharing(c.Request.Context(), token, uuid, target, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionAddSharing(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
target, okG := form["target"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okG || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionAddSharing(c.Request.Context(), token, uuid, target, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CollectionGetShared(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.CollectionGetShared(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Todo
|
||||||
|
|
||||||
|
func (h *Handler) TodoGetFull(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.TodoGetFull(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TodoGetList(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.TodoGetList(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TodoGetDetail(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.TodoGetDetail(c.Request.Context(), token, uuid)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TodoAdd(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.TodoAdd(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TodoUpdate(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
dataField, okD := form["data"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okD || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.TodoUpdate(c.Request.Context(), token, uuid, dataField, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TodoDelete(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
uuid, okU := form["uuid"]
|
||||||
|
lastChange, okL := form["lastChange"]
|
||||||
|
if !okT || !okU || !okL {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.TodoDelete(c.Request.Context(), token, uuid, lastChange)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Admin
|
||||||
|
|
||||||
|
func (h *Handler) AdminGet(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.AdminGet(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AdminAdd(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
username, okU := form["username"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.AdminAdd(c.Request.Context(), token, username)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AdminUpdate(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
username, okU := form["username"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts database.AdminUpdateOptions
|
||||||
|
provided := 0
|
||||||
|
if v, ok := form["password"]; ok {
|
||||||
|
opts.Password = &v
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if v, ok := form["isAdmin"]; ok {
|
||||||
|
b := utils.Str2Bool(v)
|
||||||
|
opts.IsAdmin = &b
|
||||||
|
provided++
|
||||||
|
}
|
||||||
|
if provided == 0 {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.DB.AdminUpdate(c.Request.Context(), token, username, opts)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AdminDelete(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
username, okU := form["username"]
|
||||||
|
if !okT || !okU {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.AdminDelete(c.Request.Context(), token, username)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Profile
|
||||||
|
|
||||||
|
func (h *Handler) ProfileIsAdmin(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.ProfileIsAdmin(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ProfileChangePassword(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
password, okP := form["password"]
|
||||||
|
if !okT || !okP {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.ProfileChangePassword(c.Request.Context(), token, password)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ProfileGetToken(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, ok := form["token"]
|
||||||
|
if !ok {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.ProfileGetToken(c.Request.Context(), token)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ProfileDeleteToken(c *gin.Context) {
|
||||||
|
form := fetchForm(c, h.Logger)
|
||||||
|
token, okT := form["token"]
|
||||||
|
deleteToken, okD := form["deleteToken"]
|
||||||
|
if !okT || !okD {
|
||||||
|
respondInvalidParam(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := h.DB.ProfileDeleteToken(c.Request.Context(), token, deleteToken)
|
||||||
|
respond(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// endregion
|
||||||
@@ -50,9 +50,9 @@ func GenerateToken(username string) string {
|
|||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSalt returns a random salt in the inclusive range [0, 6172748].
|
// GenerateSalt returns a random salt in the inclusive range [0, 6172748).
|
||||||
func GenerateSalt() int {
|
func GenerateSalt() int {
|
||||||
return rand.Intn(6172749)
|
return rand.Intn(6172748)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComputePasswordHashWithSalt returns the lowercased SHA-256 hex digest of
|
// ComputePasswordHashWithSalt returns the lowercased SHA-256 hex digest of
|
||||||
@@ -80,7 +80,7 @@ func Str2Bool(s string) bool {
|
|||||||
|
|
||||||
// GCD returns the greatest common divisor of a and b via the Euclidean
|
// GCD returns the greatest common divisor of a and b via the Euclidean
|
||||||
// algorithm.
|
// algorithm.
|
||||||
func GCD(a, b int) int {
|
func GCD(a, b int64) int64 {
|
||||||
for b != 0 {
|
for b != 0 {
|
||||||
a, b = b, a%b
|
a, b = b, a%b
|
||||||
}
|
}
|
||||||
@@ -88,6 +88,6 @@ func GCD(a, b int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LCM returns the least common multiple of a and b.
|
// LCM returns the least common multiple of a and b.
|
||||||
func LCM(a, b int) int {
|
func LCM(a, b int64) int64 {
|
||||||
return (a * b) / GCD(a, b)
|
return (a * b) / GCD(a, b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
div.user-item {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.user-item-words {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.user-item-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.token-item {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.token-item-words {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.token-item-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.control-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.control-list > * {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
#ccn-calendar-calendarBody > div:nth-child(n+2) > div {
|
|
||||||
border-top: 0 solid black;
|
|
||||||
border-left: 0 solid black;
|
|
||||||
border-right: 1px solid black;
|
|
||||||
border-bottom: 1px solid black;
|
|
||||||
|
|
||||||
padding: 0.75em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ccn-calendar-calendarBody > div:nth-child(n+2) > div:nth-child(1) {
|
|
||||||
border-left: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ccn-calendar-calendarBody > div:nth-child(2) > div {
|
|
||||||
border-top: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ccn-calendar-calendarBody > div > div {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ccn-calendar-calendarBody > div > div[isCurrentMonth=false] {
|
|
||||||
background: #d0d0d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ccn-calendar-calendarBody > div {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.calendarItem-eventBox {
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
height: 0.75rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.calendarItem-eventBox[enableDisplay=true] {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
div.calendarItem-eventBox[enableDisplay=false] {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.schedule-day {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-day-words {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-event-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-event-outter {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-event-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-event-words {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-event-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.schedule-event-color {
|
|
||||||
width: 0.75rem;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ccn-calendar-scheduleList div.schedule-day:nth-child(n+2) {
|
|
||||||
border-top: 1px solid rgba(219,219,219,.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.collection-item {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.collection-item-words {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.collection-item-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.control-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.control-list > * {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
div.collection-item {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.collection-item-words {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.collection-item-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.control-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.control-list > * {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
div.perfectTable {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
div.perfectTable > div > div:nth-child(1) {
|
|
||||||
border-left: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.perfectTable > div:nth-child(1) > div {
|
|
||||||
border-top: 1px solid black;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.perfectTable > div > div {
|
|
||||||
/*border-top: 0 solid black;
|
|
||||||
border-left: 0 solid black;
|
|
||||||
border-right: 1px solid black;
|
|
||||||
border-bottom: 1px solid black;
|
|
||||||
|
|
||||||
padding: 0.75em;*/
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.perfectTable > div {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.perfectTable > div > div[picked=true] {
|
|
||||||
background: hsl(171, 100%, 41%);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.pickerContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-flow: row;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.pickerContainer > div {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.pickerContainer > svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.pickerContainer > svg > text {
|
|
||||||
dominant-baseline: middle;
|
|
||||||
text-anchor: middle;
|
|
||||||
user-select: none;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.pickerContainer > svg > circle[type=background] {
|
|
||||||
stroke-width: 0;
|
|
||||||
fill: #d0d0d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.pickerContainer > svg > circle[type=symbol] {
|
|
||||||
stroke-width: 0;
|
|
||||||
fill: hsl(171, 100%, 41%); /* $primary */
|
|
||||||
}
|
|
||||||
|
|
||||||
div.pickerContainer > svg > line {
|
|
||||||
stroke-width: 0.125em;
|
|
||||||
stroke: hsl(171, 100%, 41%); /* $primary */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
header.pickerHeader {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
header.pickerHeader > div {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
div.control-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.control-list > * {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#ccn-event-eventFormBody > section {
|
|
||||||
border-top: 1px solid rgba(219,219,219,.5);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
div.todo-item {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.todo-item-words {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.todo-item-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div.control-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.control-list > * {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
ccn-i18n-pageName-home=coconut-leaf - A light, self-host calendar system.
|
|
||||||
ccn-i18n-pageName-collection=coconut-leaf - Collection
|
|
||||||
ccn-i18n-pageName-calendar=coconut-leaf - Calendar
|
|
||||||
ccn-i18n-pageName-event=coconut-leaf - Event
|
|
||||||
ccn-i18n-pageName-todo=coconut-leaf - Todo
|
|
||||||
ccn-i18n-pageName-admin=coconut-leaf - Admin
|
|
||||||
ccn-i18n-pageName-login=coconut-leaf - Login
|
|
||||||
|
|
||||||
ccn-i18n-header-nav-home=Home
|
|
||||||
ccn-i18n-header-nav-collection=Collection
|
|
||||||
ccn-i18n-header-nav-calendar=Calendar
|
|
||||||
ccn-i18n-header-nav-todo=Todo
|
|
||||||
ccn-i18n-header-nav-admin=Admin
|
|
||||||
ccn-i18n-header-user-login=Login
|
|
||||||
ccn-i18n-header-user-logout=Logout
|
|
||||||
ccn-i18n-header-language=Language
|
|
||||||
|
|
||||||
ccn-i18n-universal-text-year=Year
|
|
||||||
ccn-i18n-universal-text-month=Month
|
|
||||||
ccn-i18n-universal-text-day=Day
|
|
||||||
ccn-i18n-universal-text-hour=Hour
|
|
||||||
ccn-i18n-universal-text-minute=Minute
|
|
||||||
ccn-i18n-universal-week-1=Monday
|
|
||||||
ccn-i18n-universal-week-2=Tuesday
|
|
||||||
ccn-i18n-universal-week-3=Wednesday
|
|
||||||
ccn-i18n-universal-week-4=Thursday
|
|
||||||
ccn-i18n-universal-week-5=Friday
|
|
||||||
ccn-i18n-universal-week-6=Saturday
|
|
||||||
ccn-i18n-universal-week-7=Sunday
|
|
||||||
ccn-i18n-universal-month-1=January
|
|
||||||
ccn-i18n-universal-month-2=February
|
|
||||||
ccn-i18n-universal-month-3=March
|
|
||||||
ccn-i18n-universal-month-4=April
|
|
||||||
ccn-i18n-universal-month-5=May
|
|
||||||
ccn-i18n-universal-month-6=June
|
|
||||||
ccn-i18n-universal-month-7=July
|
|
||||||
ccn-i18n-universal-month-8=August
|
|
||||||
ccn-i18n-universal-month-9=September
|
|
||||||
ccn-i18n-universal-month-10=October
|
|
||||||
ccn-i18n-universal-month-11=November
|
|
||||||
ccn-i18n-universal-month-12=December
|
|
||||||
|
|
||||||
ccn-i18n-messagebox-confirm=OK
|
|
||||||
ccn-i18n-messagebox-title=Notification
|
|
||||||
|
|
||||||
ccn-i18n-datetimepicker-confirm=OK
|
|
||||||
ccn-i18n-datetimepicker-cancel=Cancel
|
|
||||||
|
|
||||||
ccn-i18n-js-fail-login=Fail to login. Please check your username or password.
|
|
||||||
ccn-i18n-js-fail-logout=Fail to logout due to unknow reason. Consider refreshing page to solve problem.
|
|
||||||
ccn-i18n-js-fail-get=A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.
|
|
||||||
ccn-i18n-js-fail-add=An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.
|
|
||||||
ccn-i18n-js-fail-update=An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.
|
|
||||||
ccn-i18n-js-fail-delete=A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.
|
|
||||||
ccn-i18n-js-success=Operation OK.
|
|
||||||
ccn-i18n-js-fail-form=Your filled event form is not fufilled or have error. Please check it and try again.
|
|
||||||
|
|
||||||
ccn-i18n-home-desc=<h1 class="title">coconut-leaf</h1><p>A light, self-host calendar system.</p><p>Originally, this app is served for yyc12345 personal use.</p><br /><p>Pull request / issue / translation are welcomed.</p><p>Submit them in our <a href="https://github.com/yyc12345/coconut-leaf">GitHub project</a>.</p><p>This project source code is licensed <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL v3</a>.</p>
|
|
||||||
|
|
||||||
ccn-i18n-login-form-username=Username
|
|
||||||
ccn-i18n-login-form-password=Password
|
|
||||||
ccn-i18n-login-form-login=Login
|
|
||||||
|
|
||||||
ccn-i18n-todo-todoList=Todo list
|
|
||||||
|
|
||||||
ccn-i18n-calendar-calendar-today=Today
|
|
||||||
ccn-i18n-calendar-calendar-add=Add...
|
|
||||||
ccn-i18n-calendar-calendar-stripedEvents={0} items
|
|
||||||
ccn-i18n-calendar-calendar-scheduleList=Schedule
|
|
||||||
ccn-i18n-calendar-tabcontrol-tabCalendar=Calendar
|
|
||||||
ccn-i18n-calendar-tabcontrol-tabCollection=Collection
|
|
||||||
ccn-i18n-calendar-tabcontrol-tabDisplay=Display
|
|
||||||
ccn-i18n-calendar-owned-list=My collections
|
|
||||||
ccn-i18n-calendar-shared-list=Shared collections
|
|
||||||
ccn-i18n-calendar-display-firstDayOfWeek=The first day of week
|
|
||||||
ccn-i18n-calendar-display-subcalendar=Sub-Calendar
|
|
||||||
|
|
||||||
ccn-i18n-calendar-display-subcalendar-chineseLunisolarCalendar=Chinese Lunisolar Calendar
|
|
||||||
|
|
||||||
ccn-i18n-collection-owned-list=Owned
|
|
||||||
ccn-i18n-collection-sharing-list=Sharing target
|
|
||||||
ccn-i18n-collection-sharing-editing=Editing:
|
|
||||||
|
|
||||||
ccn-i18n-event-header=Edit Event
|
|
||||||
ccn-i18n-event-title=Title
|
|
||||||
ccn-i18n-event-description=Description
|
|
||||||
ccn-i18n-event-color=Color
|
|
||||||
ccn-i18n-event-collection=Collection
|
|
||||||
ccn-i18n-event-startDateTime=Start Date Time
|
|
||||||
ccn-i18n-event-endDateTime=Stop Date Time
|
|
||||||
ccn-i18n-event-btnSpot=Spot
|
|
||||||
ccn-i18n-event-btnFullDay=Full day
|
|
||||||
ccn-i18n-event-loop=Event Loop
|
|
||||||
ccn-i18n-event-loop-never=Never
|
|
||||||
ccn-i18n-event-loop-day=Day
|
|
||||||
ccn-i18n-event-loop-week=Week
|
|
||||||
ccn-i18n-event-loop-month=Month
|
|
||||||
ccn-i18n-event-loop-year=Year
|
|
||||||
ccn-i18n-event-loopDay-span=Day span
|
|
||||||
ccn-i18n-event-loopWeek-span=Week span
|
|
||||||
ccn-i18n-event-loopWeek-option=Week options
|
|
||||||
ccn-i18n-event-loopMonth-span=Month span
|
|
||||||
ccn-i18n-event-loopWeek-option=Month mode
|
|
||||||
ccn-i18n-event-loopWeek-optionA=Day {0} in month
|
|
||||||
ccn-i18n-event-loopWeek-optionB=Day {0} from the end of the month
|
|
||||||
ccn-i18n-event-loopWeek-optionC=Day {1} in week {0}
|
|
||||||
ccn-i18n-event-loopWeek-optionD=Day {1} in week {0} from the end of the month
|
|
||||||
ccn-i18n-event-loopYear-span=Year span
|
|
||||||
ccn-i18n-event-loopStop=Event Loop Stop
|
|
||||||
ccn-i18n-event-loopStop-forever=Forever
|
|
||||||
ccn-i18n-event-loopStop-datetime=Date Time
|
|
||||||
ccn-i18n-event-loopStop-times=Times
|
|
||||||
ccn-i18n-event-timezone-title=Timezone
|
|
||||||
ccn-i18n-event-timezone-warning=The timezone of this event is not corresponding with your current timezone. All of date and time in this page are shown as the original timezone of this event. You can choose a timezone option in follwing content. If you are not familar with this, please pick keep timezone.
|
|
||||||
ccn-i18n-event-timezone-keep=Keep timezone
|
|
||||||
ccn-i18n-event-timezone-replace=Use my timezone
|
|
||||||
ccn-i18n-event-strictMode-title=Strict Mode in Event Loop
|
|
||||||
ccn-i18n-event-strictMode-warning=You can choose strict mode or rough mode in following content. This is only effect on looped event.
|
|
||||||
ccn-i18n-event-strictMode-strict=Strict Mode. If ordered day is not existing, skip it.
|
|
||||||
ccn-i18n-event-strictMode-rough=Rough mode. If ordered day is not existing, choose the day closing with original day to arrange event.
|
|
||||||
ccn-i18n-event-btnSubmit=Submit
|
|
||||||
ccn-i18n-event-btnCancel=Cancel
|
|
||||||
|
|
||||||
ccn-i18n-sharedItem-sharedBy=Shared by:
|
|
||||||
|
|
||||||
ccn-i18n-admin-tabcontrol-tabProfile=My Profile
|
|
||||||
ccn-i18n-admin-tabcontrol-tabToken=Manage Multi-login
|
|
||||||
ccn-i18n-admin-tabcontrol-tabUserList=Manager User
|
|
||||||
ccn-i18n-admin-changePassword=Change Password
|
|
||||||
ccn-i18n-admin-manageToken=Manage multi-login
|
|
||||||
ccn-i18n-admin-manageToken-desc=Manage the multi-login of the current account. You can forced logout some login in there.
|
|
||||||
ccn-i18n-admin-userList=User List
|
|
||||||
|
|
||||||
ccn-i18n-userItem-newPassword=New Password
|
|
||||||
ccn-i18n-userItem-isAdmin=Is Admin
|
|
||||||
|
|
||||||
ccn-i18n-tokenItem-ua=User Agent:
|
|
||||||
ccn-i18n-tokenItem-ip=IP:
|
|
||||||
ccn-i18n-tokenItem-expireOn=Expire On:
|
|
||||||
ccn-i18n-tokenItem-isMe=This is the login credentials you are currently using.
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
ccn-i18n-pageName-home=coconut-leaf - 一个轻量的自建日历系统
|
|
||||||
ccn-i18n-pageName-collection=coconut-leaf - 集合
|
|
||||||
ccn-i18n-pageName-calendar=coconut-leaf - 日历
|
|
||||||
ccn-i18n-pageName-event=coconut-leaf - 事件
|
|
||||||
ccn-i18n-pageName-todo=coconut-leaf - 待办
|
|
||||||
ccn-i18n-pageName-admin=coconut-leaf - 管理
|
|
||||||
ccn-i18n-pageName-login=coconut-leaf - 登录
|
|
||||||
|
|
||||||
ccn-i18n-header-nav-home=主页
|
|
||||||
ccn-i18n-header-nav-collection=集合
|
|
||||||
ccn-i18n-header-nav-calendar=日历
|
|
||||||
ccn-i18n-header-nav-todo=待办
|
|
||||||
ccn-i18n-header-nav-admin=管理
|
|
||||||
ccn-i18n-header-user-login=登录
|
|
||||||
ccn-i18n-header-user-logout=登出
|
|
||||||
ccn-i18n-header-language=语言
|
|
||||||
|
|
||||||
ccn-i18n-universal-text-year=年
|
|
||||||
ccn-i18n-universal-text-month=月
|
|
||||||
ccn-i18n-universal-text-day=日
|
|
||||||
ccn-i18n-universal-text-hour=时
|
|
||||||
ccn-i18n-universal-text-minute=分
|
|
||||||
ccn-i18n-universal-week-1=星期一
|
|
||||||
ccn-i18n-universal-week-2=星期二
|
|
||||||
ccn-i18n-universal-week-3=星期三
|
|
||||||
ccn-i18n-universal-week-4=星期四
|
|
||||||
ccn-i18n-universal-week-5=星期五
|
|
||||||
ccn-i18n-universal-week-6=星期六
|
|
||||||
ccn-i18n-universal-week-7=星期日
|
|
||||||
ccn-i18n-universal-month-1=1月
|
|
||||||
ccn-i18n-universal-month-2=2月
|
|
||||||
ccn-i18n-universal-month-3=3月
|
|
||||||
ccn-i18n-universal-month-4=4月
|
|
||||||
ccn-i18n-universal-month-5=5月
|
|
||||||
ccn-i18n-universal-month-6=6月
|
|
||||||
ccn-i18n-universal-month-7=7月
|
|
||||||
ccn-i18n-universal-month-8=8月
|
|
||||||
ccn-i18n-universal-month-9=9月
|
|
||||||
ccn-i18n-universal-month-10=10月
|
|
||||||
ccn-i18n-universal-month-11=11月
|
|
||||||
ccn-i18n-universal-month-12=12月
|
|
||||||
|
|
||||||
ccn-i18n-messagebox-confirm=确认
|
|
||||||
ccn-i18n-messagebox-title=通知
|
|
||||||
|
|
||||||
ccn-i18n-datetimepicker-confirm=确认
|
|
||||||
ccn-i18n-datetimepicker-cancel=取消
|
|
||||||
|
|
||||||
ccn-i18n-js-fail-login=登陆失败,请检查您的用户名和密码。
|
|
||||||
ccn-i18n-js-fail-logout=由于未知原因,登出失败,请考虑刷新页面解决问题。
|
|
||||||
ccn-i18n-js-fail-get=一个获取操作失败了,可能是系统错误或者您的权限不足。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。
|
|
||||||
ccn-i18n-js-fail-add=一个添加操作失败了,可能是您输入的参数有误。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。
|
|
||||||
ccn-i18n-js-fail-update=一个更新操作失败了,可能是没有找到匹配的条目或者您的参数输入错误。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。
|
|
||||||
ccn-i18n-js-fail-delete=一个删除操作失败了,可能是没有找到对应条目。刷新页面可能会解决问题。请在刷新页面前备份好自己的数据。
|
|
||||||
ccn-i18n-js-success=操作成功
|
|
||||||
ccn-i18n-js-fail-form=您所填写的事件内容存在缺漏或有错误字段,请检查后再提交。
|
|
||||||
|
|
||||||
ccn-i18n-home-desc=<h1 class="title">coconut-leaf</h1><p>一个轻量的自建日历系统</p><p>原本是出于yyc12345的个人使用而制作的。</p><br /><p>欢迎提出Pull request / issue / 翻译</p><p>将他们提交到我们的<a href="https://github.com/yyc12345/coconut-leaf">GitHub项目</a>.</p><p>本工程代码使用<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL v3</a>授权。</p>
|
|
||||||
|
|
||||||
ccn-i18n-login-form-username=用户名
|
|
||||||
ccn-i18n-login-form-password=密码
|
|
||||||
ccn-i18n-login-form-login=登录
|
|
||||||
|
|
||||||
ccn-i18n-todo-todoList=待办列表
|
|
||||||
|
|
||||||
ccn-i18n-calendar-calendar-today=今天
|
|
||||||
ccn-i18n-calendar-calendar-add=添加...
|
|
||||||
ccn-i18n-calendar-calendar-stripedEvents=共{0}项
|
|
||||||
ccn-i18n-calendar-calendar-scheduleList=日程安排
|
|
||||||
ccn-i18n-calendar-tabcontrol-tabCalendar=日历
|
|
||||||
ccn-i18n-calendar-tabcontrol-tabCollection=集合
|
|
||||||
ccn-i18n-calendar-tabcontrol-tabDisplay=显示设置
|
|
||||||
ccn-i18n-calendar-owned-list=我的集合
|
|
||||||
ccn-i18n-calendar-shared-list=被共享的集合
|
|
||||||
ccn-i18n-calendar-display-firstDayOfWeek=每周开始星期
|
|
||||||
ccn-i18n-calendar-display-subcalendar=副日历
|
|
||||||
|
|
||||||
ccn-i18n-calendar-display-subcalendar-chineseLunisolarCalendar=中国农历
|
|
||||||
|
|
||||||
ccn-i18n-collection-owned-list=我的集合
|
|
||||||
ccn-i18n-collection-sharing-list=分享目标
|
|
||||||
ccn-i18n-collection-sharing-editing=正在编辑集合:
|
|
||||||
|
|
||||||
ccn-i18n-event-header=编辑事件
|
|
||||||
ccn-i18n-event-title=标题
|
|
||||||
ccn-i18n-event-description=描述
|
|
||||||
ccn-i18n-event-color=颜色
|
|
||||||
ccn-i18n-event-collection=集合
|
|
||||||
ccn-i18n-event-startDateTime=开始时间
|
|
||||||
ccn-i18n-event-endDateTime=结束时间
|
|
||||||
ccn-i18n-event-btnSpot=时间点
|
|
||||||
ccn-i18n-event-btnFullDay=全天
|
|
||||||
ccn-i18n-event-loop=事件循环
|
|
||||||
ccn-i18n-event-loop-never=从不
|
|
||||||
ccn-i18n-event-loop-day=按天
|
|
||||||
ccn-i18n-event-loop-week=按周
|
|
||||||
ccn-i18n-event-loop-month=按月
|
|
||||||
ccn-i18n-event-loop-year=按年
|
|
||||||
ccn-i18n-event-loopDay-span=间隔天数
|
|
||||||
ccn-i18n-event-loopWeek-span=间隔周数
|
|
||||||
ccn-i18n-event-loopWeek-option=在下列这些星期上循环
|
|
||||||
ccn-i18n-event-loopMonth-span=间隔月数
|
|
||||||
ccn-i18n-event-loopWeek-option=月份模式
|
|
||||||
ccn-i18n-event-loopWeek-optionA=第{0}天
|
|
||||||
ccn-i18n-event-loopWeek-optionB=倒数第{0}天
|
|
||||||
ccn-i18n-event-loopWeek-optionC=第{0}个星期{1}
|
|
||||||
ccn-i18n-event-loopWeek-optionD=倒数第{0}个星期{1}
|
|
||||||
ccn-i18n-event-loopYear-span=间隔年数
|
|
||||||
ccn-i18n-event-loopStop=事件循环停止方式
|
|
||||||
ccn-i18n-event-loopStop-forever=永不停止
|
|
||||||
ccn-i18n-event-loopStop-datetime=指定时间
|
|
||||||
ccn-i18n-event-loopStop-times=指定次数
|
|
||||||
ccn-i18n-event-timezone-title=时区设定
|
|
||||||
ccn-i18n-event-timezone-warning=您当前设置的事件的时区与您的时区不匹配,本页面以事件的原时区进行时间显示。您可以在下面修改您对于此事件的时区选择,如果您不熟悉时区,请选择保持原有时区。
|
|
||||||
ccn-i18n-event-timezone-keep=保持原有时区
|
|
||||||
ccn-i18n-event-timezone-replace=使用我现在的时区
|
|
||||||
ccn-i18n-event-strictMode-title=循环的严格与宽松
|
|
||||||
ccn-i18n-event-strictMode-warning=允许您在严格模式与宽松模式种进行选择,此选择只对循环事件有效。
|
|
||||||
ccn-i18n-event-strictMode-strict=严格模式,严格遵守日期要求,如果日期不存在,就不安排。
|
|
||||||
ccn-i18n-event-strictMode-rough=宽松模式,不在意日期要求的精确性,如果日期不存在,则找到最近的符合条件的日子安排。
|
|
||||||
ccn-i18n-event-btnSubmit=提交
|
|
||||||
ccn-i18n-event-btnCancel=取消
|
|
||||||
|
|
||||||
ccn-i18n-sharedItem-sharedBy=共享人:
|
|
||||||
|
|
||||||
ccn-i18n-admin-tabcontrol-tabProfile=我的资料
|
|
||||||
ccn-i18n-admin-tabcontrol-tabToken=管理多端登录
|
|
||||||
ccn-i18n-admin-tabcontrol-tabUserList=管理用户
|
|
||||||
ccn-i18n-admin-changePassword=更改密码
|
|
||||||
ccn-i18n-admin-manageToken=管理多端登录
|
|
||||||
ccn-i18n-admin-manageToken-desc=管理当前帐号的多端登录。您可以在这里强制下线某些地方的帐号。
|
|
||||||
ccn-i18n-admin-userList=用户列表
|
|
||||||
|
|
||||||
ccn-i18n-userItem-newPassword=新密码
|
|
||||||
ccn-i18n-userItem-isAdmin=是管理员
|
|
||||||
|
|
||||||
ccn-i18n-tokenItem-ua=UA:
|
|
||||||
ccn-i18n-tokenItem-ip=IP:
|
|
||||||
ccn-i18n-tokenItem-expireOn=过期时间:
|
|
||||||
ccn-i18n-tokenItem-isMe=这是你当前使用的登录凭据
|
|
||||||
|
|
||||||
ccn-i18n-datetime-loopStopRuleText-infinity=永远循环。
|
|
||||||
ccn-i18n-datetime-loopStopRuleText-datetime=到{0}停止循环。
|
|
||||||
ccn-i18n-datetime-loopStopRuleText-times=循环{0}次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-modeStrict=严格模式。
|
|
||||||
ccn-i18n-datetime-loopRuleText-modeRough=宽松模式。
|
|
||||||
ccn-i18n-datetime-loopRuleText-year=每{0}年于{1}循环一次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-monthA=每{0}月的第{1}日循环一次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-monthB=每{0}月的倒数第{1}日循环一次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-monthC=每{0}月的第{1}个星期{2}循环一次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-monthD=每{0}月的倒数第{1}个星期{2}循环一次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-week=每{0}周的{1}循环一次。
|
|
||||||
ccn-i18n-datetime-loopRuleText-day=每{0}天循环一次。
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
@@ -1,483 +0,0 @@
|
|||||||
// the api use bool to return status: fail: return false, true: return data(including true and false)
|
|
||||||
// the api use other type to return data: fail: return undefined, true: return data(if the returned value have change be null, return undefined instaed).
|
|
||||||
|
|
||||||
// var cached_salt = undefined
|
|
||||||
|
|
||||||
/*
|
|
||||||
function ccn_api_common_salt(_username) {
|
|
||||||
// true or false
|
|
||||||
// gotten salt store in cached_salt.
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/common/salt',
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: {
|
|
||||||
username: _username
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IsResponseOK(gotten_data)) {
|
|
||||||
cached_salt = gotten_data['data'];
|
|
||||||
return true;
|
|
||||||
} else return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_common_login(_username, password) {
|
|
||||||
// return true or false, token is managed by this js file self.
|
|
||||||
// if cached_salt is undefined, return false directly
|
|
||||||
if (typeof(cached_salt) == undefined) return false;
|
|
||||||
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/common/login',
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: {
|
|
||||||
username: _username,
|
|
||||||
password: ComputPasswordWithSalt(password, cached_salt)
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (IsResponseOK(gotten_data) && gotten_data['data'] != '') {
|
|
||||||
ccn_localstorageAssist_SetApiToken(gotten_data['data']);
|
|
||||||
cached_salt = undefined;
|
|
||||||
return true;
|
|
||||||
} else return false;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ============================================ template
|
|
||||||
// all api can be implemented by these 2 function, except 3 token related func.
|
|
||||||
// so all api func should use these 2 func except 3 token process api.
|
|
||||||
function ccn_api_dataTemplate(_url, _data) {
|
|
||||||
// return data or undefined
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: _url,
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: _data,
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IsResponseOK(gotten_data) && !(gotten_data['data'] === null)) return gotten_data['data'];
|
|
||||||
else return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_boolTemplate(_url, _data) {
|
|
||||||
// return true or false
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: _url,
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: _data,
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (IsResponseOK(gotten_data) && gotten_data['data']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// deserialize & serialize calendar description function
|
|
||||||
|
|
||||||
function ccn_api_serializeDescription(_description, _color) {
|
|
||||||
var sobj = {
|
|
||||||
description: _description,
|
|
||||||
color: _color
|
|
||||||
}
|
|
||||||
return JSON.stringify(sobj);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_deserializeDescription(strl) {
|
|
||||||
try {
|
|
||||||
return $.parseJSON(strl);
|
|
||||||
} catch(err) {
|
|
||||||
return {
|
|
||||||
description: "",
|
|
||||||
color: DefaultColor
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================================================== common
|
|
||||||
|
|
||||||
function ccn_api_common_webLogin(_username, _password) {
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/common/webLogin',
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: {
|
|
||||||
username: _username,
|
|
||||||
password: _password
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (IsResponseOK(gotten_data)) {
|
|
||||||
ccn_localstorageAssist_SetApiToken(gotten_data['data']);
|
|
||||||
return true;
|
|
||||||
} else return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_common_logout() {
|
|
||||||
// return true or false
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/common/logout',
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: {
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IsResponseOK(gotten_data) && gotten_data['data']) {
|
|
||||||
ccn_localstorageAssist_SetApiToken('');
|
|
||||||
return true;
|
|
||||||
} return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_common_tokenValid() {
|
|
||||||
// get from local database first, then judge it via post
|
|
||||||
// return true or false
|
|
||||||
var gotten_token = ccn_localstorageAssist_GetApiToken();
|
|
||||||
if (gotten_token == '') return false;
|
|
||||||
|
|
||||||
var gotten_data = undefined;
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/common/tokenValid',
|
|
||||||
type: "POST",
|
|
||||||
async: false,
|
|
||||||
data: {
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
gotten_data = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IsResponseOK(gotten_data) && gotten_data['data']) return true;
|
|
||||||
else {
|
|
||||||
ccn_localstorageAssist_SetApiToken('');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================================================== calendar
|
|
||||||
|
|
||||||
function ccn_api_calendar_getFull(_startDateTime, _endDateTime) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/calendar/getFull',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
startDateTime: _startDateTime,
|
|
||||||
endDateTime: _endDateTime
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_calendar_getDetail(_uuid) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/calendar/getDetail',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_calendar_update(_uuid, _belongTo, _title, _description, _eventDateTimeStart, _eventDateTimeEnd, _loopRules, _timezoneOffset, _lastChange) {
|
|
||||||
var data = {};
|
|
||||||
if (typeof(_belongTo) != 'undefined')
|
|
||||||
data.belongTo = _belongTo;
|
|
||||||
if (typeof(_title) != 'undefined')
|
|
||||||
data.title = _title;
|
|
||||||
if (typeof(_description) != 'undefined')
|
|
||||||
data.description = _description;
|
|
||||||
if (typeof(_eventDateTimeStart) != 'undefined')
|
|
||||||
data.eventDateTimeStart = _eventDateTimeStart;
|
|
||||||
if (typeof(_eventDateTimeEnd) != 'undefined')
|
|
||||||
data.eventDateTimeEnd = _eventDateTimeEnd;
|
|
||||||
if (typeof(_loopRules) != 'undefined')
|
|
||||||
data.loopRules = _loopRules;
|
|
||||||
if (typeof(_timezoneOffset) != 'undefined')
|
|
||||||
data.timezoneOffset = _timezoneOffset;
|
|
||||||
|
|
||||||
data.token = ccn_localstorageAssist_GetApiToken();
|
|
||||||
data.uuid = _uuid;
|
|
||||||
data.lastChange = _lastChange;
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/calendar/update',
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_calendar_add(_belongTo, _title, _description, _eventDateTimeStart, _eventDateTimeEnd, _loopRules, _timezoneOffset) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/calendar/add',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
belongTo: _belongTo,
|
|
||||||
title: _title,
|
|
||||||
description: _description,
|
|
||||||
eventDateTimeStart: _eventDateTimeStart,
|
|
||||||
eventDateTimeEnd: _eventDateTimeEnd,
|
|
||||||
loopRules: _loopRules,
|
|
||||||
timezoneOffset: _timezoneOffset
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_calendar_delete(_uuid, _lastChange) {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/calendar/delete',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================================================== collection
|
|
||||||
|
|
||||||
function ccn_api_collection_getFullOwn() {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/getFullOwn',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_getDetailOwn(_uuid) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/getDetailOwn',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_addOwn(_name) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/addOwn',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
name: _name
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_updateOwn(_uuid, _name, _lastChange) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/updateOwn',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
name: _name,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_deleteOwn(_uuid, _lastChange) {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/collection/deleteOwn',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_getSharing(_uuid) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/getSharing',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_deleteSharing(_uuid, _target, _lastChange) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/deleteSharing',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
target: _target,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_addSharing(_uuid, _target, _lastChange) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/addSharing',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
target: _target,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_collection_getShared() {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/collection/getShared',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================================================== todo
|
|
||||||
|
|
||||||
function ccn_api_todo_getFull() {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/todo/getFull',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_todo_add() {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/todo/add',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_todo_update(_uuid, _data, _lastChange) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/todo/update',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
data: _data,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_todo_delete(_uuid, _lastChange) {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/todo/delete',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
uuid: _uuid,
|
|
||||||
lastChange: _lastChange
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================================================== admin
|
|
||||||
|
|
||||||
function ccn_api_admin_get() {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/admin/get',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_admin_add(_username) {
|
|
||||||
return ccn_api_dataTemplate(
|
|
||||||
'/api/admin/add',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
username: _username
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_admin_update(_username, _password, _isAdmin) {
|
|
||||||
var data = {};
|
|
||||||
if (typeof(_password) != 'undefined')
|
|
||||||
data.password = _password;
|
|
||||||
if (typeof(_isAdmin) != 'undefined')
|
|
||||||
data.isAdmin = _isAdmin;
|
|
||||||
|
|
||||||
if (Object.getOwnPropertyNames(data).length == 0) return false;
|
|
||||||
data.token = ccn_localstorageAssist_GetApiToken();
|
|
||||||
data.username = _username;
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/admin/update',
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_admin_delete(_username) {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/admin/delete',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
username: _username
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================================================== profile
|
|
||||||
|
|
||||||
function ccn_api_profile_isAdmin() {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/profile/isAdmin',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_profile_changePassword(_password) {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/profile/changePassword',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
password: _password
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_profile_getToken() {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/profile/getToken',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_api_profile_deleteToken(_deleteToken) {
|
|
||||||
return ccn_api_boolTemplate(
|
|
||||||
'/api/profile/deleteToken',
|
|
||||||
{
|
|
||||||
token: ccn_localstorageAssist_GetApiToken(),
|
|
||||||
deleteToken: _deleteToken
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
// NOTE: this file is sync with dt.py. if this file or dt.py have bugs, all code should be changed
|
|
||||||
var ccn_datetime_monthDayCount = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
||||||
|
|
||||||
var ccn_datetime_MIN_YEAR = 1950;
|
|
||||||
var ccn_datetime_MAX_YEAR = 2200;
|
|
||||||
var ccn_datetime_MIN_DATETIME = new Date(Date.UTC(ccn_datetime_MIN_YEAR, 0, 1, 0, 0, 0, 0));
|
|
||||||
var ccn_datetime_MAX_DATETIME = new Date(Date.UTC(ccn_datetime_MAX_YEAR, 0, 1, 0, 0, 0, 0));
|
|
||||||
var ccn_datetime_MIN_TIMESTAMP = Math.floor(ccn_datetime_MIN_DATETIME.getTime() / 60000);
|
|
||||||
var ccn_datetime_MAX_TIMESTAMP = Math.floor(ccn_datetime_MAX_DATETIME.getTime() / 60000);
|
|
||||||
|
|
||||||
var ccn_datetime_DAY1_SPAN = 60 * 24;
|
|
||||||
var ccn_datetime_DAY7_SPAN = 7 * ccn_datetime_DAY1_SPAN;
|
|
||||||
|
|
||||||
var ccn_datetime_precompiledLoopRules = {
|
|
||||||
year: new RegExp(/^Y([SR]{1})([1-9]\d*)$/),
|
|
||||||
month: new RegExp(/^M([SR]{1})([ABCD]{1})([1-9]\d*)$/),
|
|
||||||
week: new RegExp(/^W([TF]{7})([1-9]\d*)$/),
|
|
||||||
day: new RegExp(/^D([1-9]\d*)$/)
|
|
||||||
};
|
|
||||||
|
|
||||||
var ccn_datetime_precompiledLoopStopRules = {
|
|
||||||
infinity: new RegExp(/^F$/),
|
|
||||||
datetime: new RegExp(/^D([1-9]\d*|0)$/),
|
|
||||||
times: new RegExp(/^T([1-9]\d*)$/)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
return format
|
|
||||||
[loopRules, loopStopRules] or undefined(invalid or no loop)
|
|
||||||
|
|
||||||
loopRules:
|
|
||||||
year loop: [0, isStrict, yearSpan]
|
|
||||||
month loop: [1, isStrict, monthMode, monthSpan]
|
|
||||||
week loop: [2, 7 bool item..., weekSpan]
|
|
||||||
day loop: [3, daySpan]
|
|
||||||
|
|
||||||
loopStopRules:
|
|
||||||
infinity: [0]
|
|
||||||
datetime: [1, timestamp]
|
|
||||||
times: [2, times]
|
|
||||||
*/
|
|
||||||
function ccn_datetime_ResolveLoopRules4UI(strl) {
|
|
||||||
if (strl == '') return undefined;
|
|
||||||
|
|
||||||
var sp = strl.split('-');
|
|
||||||
if (sp.length != 2) return undefined;
|
|
||||||
var loopRules = undefined;
|
|
||||||
var loopStopRules = undefined;
|
|
||||||
|
|
||||||
if (ccn_datetime_precompiledLoopRules.year.test(sp[0])) {
|
|
||||||
loopRules = [0, RegExp.$1 == 'S', parseInt(RegExp.$2)];
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.month.test(sp[0])) {
|
|
||||||
loopRules = [1, RegExp.$1 == 'S', RegExp.$2, parseInt(RegExp.$3)];
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.week.test(sp[0])) {
|
|
||||||
loopRules = [2];
|
|
||||||
for (var i = 0; i < 7; i++)
|
|
||||||
loopRules.push(RegExp.$1[i] == 'T');
|
|
||||||
loopRules.push(parseInt(RegExp.$2));
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.day.test(sp[0])) {
|
|
||||||
loopRules = [3, parseInt(RegExp.$1)];
|
|
||||||
} else return undefined;
|
|
||||||
|
|
||||||
|
|
||||||
if (ccn_datetime_precompiledLoopStopRules.infinity.test(sp[1])) {
|
|
||||||
loopStopRules = [0];
|
|
||||||
} else if (ccn_datetime_precompiledLoopStopRules.datetime.test(sp[1])) {
|
|
||||||
loopStopRules = [1, parseInt(RegExp.$1)];
|
|
||||||
} else if (ccn_datetime_precompiledLoopStopRules.times.test(sp[1])) {
|
|
||||||
loopStopRules = [2, parseInt(RegExp.$1)];
|
|
||||||
} else return undefined;
|
|
||||||
|
|
||||||
return [loopRules, loopStopRules];
|
|
||||||
}
|
|
||||||
|
|
||||||
// loopDateTimeStart's value is not correspond with database.
|
|
||||||
// it is calculated by program, should be pointed to the closing
|
|
||||||
// protential event start datetime.
|
|
||||||
// also loopDateTimeEnd, it was clamped with the tail of legal event
|
|
||||||
// clampStartDateTime is real clamp datetime of event start datetime.
|
|
||||||
// loopDateTimeStart is the start datetime for detect.
|
|
||||||
// in this section, all time should be analysed with Date((time + timezoneOffset) * 60000)
|
|
||||||
// and use .getUTC...() functions.
|
|
||||||
function ccn_datetime_ResolveLoopRules4Event(loopRules, loopDateTimeStart, loopDateTimeEnd, eventDateTimeStart, eventDateTimeEnd, timezoneOffset, clampStartDateTime) {
|
|
||||||
if (loopRules == '') return [
|
|
||||||
[Math.max(eventDateTimeStart, clampStartDateTime),
|
|
||||||
Math.max(loopDateTimeEnd, eventDateTimeEnd)]
|
|
||||||
];
|
|
||||||
|
|
||||||
var sp = loopRules.split('-');
|
|
||||||
if (sp.length != 2) return undefined;
|
|
||||||
var loopRules = sp[0]; // we don't need consider stop flag
|
|
||||||
var result = new Array();
|
|
||||||
|
|
||||||
// compute offset and duration
|
|
||||||
var eventDateTime = new Date((eventDateTimeStart + timezoneOffset) * 60000);
|
|
||||||
eventDateTime.setUTCHours(0, 0, 0, 0);
|
|
||||||
var eventOffset = eventDateTimeStart - (Math.floor(eventDateTime.getTime() / 60000) - timezoneOffset);
|
|
||||||
var eventDuration = eventDateTimeEnd - eventDateTimeStart;
|
|
||||||
|
|
||||||
var detectDateTime = new Date(loopDateTimeStart * 60000);
|
|
||||||
detectDateTime.setUTCHours(0, 0, 0, 0);
|
|
||||||
var originalYear = eventDateTime.getUTCFullYear();
|
|
||||||
var originalMonth = eventDateTime.getUTCMonth() + 1;
|
|
||||||
var originalDay = eventDateTime.getUTCDate();
|
|
||||||
|
|
||||||
// compute event
|
|
||||||
if (ccn_datetime_precompiledLoopRules.year.test(loopRules)) {
|
|
||||||
var isStrict = RegExp.$1 == 'S';
|
|
||||||
var loopSpan = parseInt(RegExp.$2);
|
|
||||||
|
|
||||||
var yearCount = detectDateTime.getFullYear() - originalYear;
|
|
||||||
var isSpecial = (originalMonth == 2 && originalDay == 29);
|
|
||||||
var realLoopSpan = (isSpecial && isStrict) ? LCM(4, loopSpan) : loopSpan;
|
|
||||||
|
|
||||||
//var fullSpanCount = Math.floor(yearCount / realLoopSpan);
|
|
||||||
var remainYear = yearCount % realLoopSpan;
|
|
||||||
//detectDateTime.setUTCFullYear(fullSpanCount + detectDateTime.getUTCFullYear(), 1, 1);
|
|
||||||
if (remainYear != 0)
|
|
||||||
detectDateTime.setUTCFullYear(realLoopSpan - remainYear + detectDateTime.getUTCFullYear(), 1 - 1, 1);
|
|
||||||
|
|
||||||
var skipFlag = false;
|
|
||||||
while(Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
|
|
||||||
skipFlag = false;
|
|
||||||
if (isSpecial) {
|
|
||||||
// is special day, 29 Feb
|
|
||||||
// try set it in 29 Feb
|
|
||||||
if (isStrict) {
|
|
||||||
if (ccn_datetime_IsLeapYear(detectDateTime.getUTCFullYear())) detectDateTime.setUTCMonth(2 - 1, 29);
|
|
||||||
else skipFlag = true; // order skip
|
|
||||||
} else {
|
|
||||||
if (ccn_datetime_IsLeapYear(detectDateTime.getUTCFullYear())) detectDateTime.setUTCMonth(2 - 1, 29);
|
|
||||||
else detectDateTime.setUTCMonth(2 - 1, 28);
|
|
||||||
}
|
|
||||||
} else detectDateTime.setUTCMonth(originalMonth - 1, originalDay);
|
|
||||||
|
|
||||||
if (!skipFlag) {
|
|
||||||
result.push(
|
|
||||||
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
|
|
||||||
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
detectDateTime.setUTCFullYear(realLoopSpan + detectDateTime.getUTCFullYear());
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.month.test(loopRules)) {
|
|
||||||
var isStrict = RegExp.$1 == 'S';
|
|
||||||
var loopMethod = RegExp.$2;
|
|
||||||
var loopSpan = parseInt(RegExp.$3);
|
|
||||||
|
|
||||||
var monthsCount = ccn_datetime_MonthsCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1) -
|
|
||||||
ccn_datetime_MonthsCount(originalYear, originalMonth);
|
|
||||||
|
|
||||||
//var fullSpanCount = Math.floor(monthsCount / loopSpan);
|
|
||||||
var remainMonth = monthsCount % loopSpan;
|
|
||||||
//detectDateTime.setUTCMonth(fullSpanCount * loopSpan + detectDateTime.getUTCMonth(), 1);
|
|
||||||
detectDateTime.setUTCDate(1);
|
|
||||||
if (remainMonth != 0)
|
|
||||||
detectDateTime.setUTCMonth(loopSpan - remainMonth + detectDateTime.getUTCMonth(), 1);
|
|
||||||
|
|
||||||
while(Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
|
|
||||||
var data = ccn_datetime_GetRemanagedDayInMonth(originalYear, originalMonth, originalDay, detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, isStrict);
|
|
||||||
var predictedDay = undefined;
|
|
||||||
switch(loopMethod) {
|
|
||||||
case 'A':
|
|
||||||
if (typeof(data[0]) != 'undefined') predictedDay = data[0];
|
|
||||||
break;
|
|
||||||
case 'B':
|
|
||||||
if (typeof(data[1]) != 'undefined') predictedDay = data[1];
|
|
||||||
break;
|
|
||||||
case 'C':
|
|
||||||
if (typeof(data[2]) != 'undefined') predictedDay = data[2];
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
if (typeof(data[3]) != 'undefined') predictedDay = data[3];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (typeof(predictedDay) != 'undefined') {
|
|
||||||
detectDateTime.setUTCDate(predictedDay);
|
|
||||||
result.push(
|
|
||||||
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
|
|
||||||
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
detectDateTime.setUTCMonth(loopSpan + detectDateTime.getUTCMonth(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.week.test(loopRules)) {
|
|
||||||
var loopSpan = parseInt(RegExp.$2);
|
|
||||||
var weekOption = [];
|
|
||||||
var weekEventCount = 0
|
|
||||||
for (var i = 0; i < 7; i++) {
|
|
||||||
weekOption.push(RegExp.$1[i] == 'T');
|
|
||||||
if (RegExp.$1[i] == 'T') weekEventCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var originalWeek = ccn_datetime_DayOfWeek(originalYear, originalMonth, originalDay);
|
|
||||||
|
|
||||||
// try insert original event
|
|
||||||
if (!weekOption[originalWeek]) {
|
|
||||||
result.push(
|
|
||||||
[eventDateTimeStart, eventDateTimeEnd]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var daysCount = ccn_datetime_DaysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getDate()) -
|
|
||||||
ccn_datetime_DaysCount(originalYear, originalMonth, originalDay);
|
|
||||||
//var fullSpanCount = Math.floor(daysCount / (7 * loopSpan));
|
|
||||||
var remainFullSpanCount = Math.floor((daysCount % (7 * loopSpan)) / 7);
|
|
||||||
var remainDays = (daysCount % (7 * loopSpan)) % 7;
|
|
||||||
|
|
||||||
//detectDateTime.setUTCDate((7 * loopSpan * fullSpanCount) + detectDateTime.getUTCDate());
|
|
||||||
if (remainFullSpanCount != 0) {
|
|
||||||
detectDateTime.setUTCDate((loopSpan - remainFullSpanCount) * 7 + detectDateTime.getUTCDate());
|
|
||||||
}
|
|
||||||
var weekCounter = remainDays;
|
|
||||||
|
|
||||||
while(Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
|
|
||||||
if (weekOption[(weekCounter + originalWeek) % 7])
|
|
||||||
result.push(
|
|
||||||
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
|
|
||||||
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
|
|
||||||
);
|
|
||||||
|
|
||||||
weekCounter = (weekCounter + 1) % 7;
|
|
||||||
detectDateTime.setUTCDate(detectDateTime.getUTCDate() + 1);
|
|
||||||
if (weekCounter == 0)
|
|
||||||
detectDateTime.setUTCDate(detectDateTime.getUTCDate() + (loopSpan - 1) * 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.day.test(loopRules)) {
|
|
||||||
var loopSpan = parseInt(RegExp.$1);
|
|
||||||
|
|
||||||
var daysCount = ccn_datetime_DaysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getUTCDate()) -
|
|
||||||
ccn_datetime_DaysCount(originalYear, originalMonth, originalDay);
|
|
||||||
//var fullSpanCount = Math.floor(daysCount / loopSpan);
|
|
||||||
var remainDays = daysCount % loopSpan;
|
|
||||||
//detectDateTime.setUTCDate(fullSpanCount * loopSpan + detectDateTime.getUTCDate());
|
|
||||||
if (remainDays != 0)
|
|
||||||
detectDateTime.setUTCDate(loopSpan - remainDays + detectDateTime.getUTCDate());
|
|
||||||
|
|
||||||
while(Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
|
|
||||||
result.push(
|
|
||||||
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
|
|
||||||
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
|
|
||||||
);
|
|
||||||
detectDateTime.setUTCDate(detectDateTime.getUTCDate() + loopSpan);
|
|
||||||
}
|
|
||||||
} else return undefined;
|
|
||||||
|
|
||||||
// clamp item
|
|
||||||
var realResult = [];
|
|
||||||
for (var i in result) {
|
|
||||||
var start = result[i][0];
|
|
||||||
var end = result[i][1];
|
|
||||||
if (end > clampStartDateTime && start <= loopDateTimeEnd)
|
|
||||||
realResult.push([Math.max(start, clampStartDateTime), Math.min(end, loopDateTimeEnd)]);
|
|
||||||
}
|
|
||||||
return realResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_ResolveLoopRules4Text(strl, startDateTime, timezoneOffset) {
|
|
||||||
if (strl == '') return "";
|
|
||||||
|
|
||||||
var sp = strl.split('-');
|
|
||||||
if (sp.length != 2) return "";
|
|
||||||
var loopRules = undefined;
|
|
||||||
var loopStopRules = undefined;
|
|
||||||
var datetimeInstance = new Date((startDateTime + timezoneOffset) * 60000)
|
|
||||||
|
|
||||||
if (ccn_datetime_precompiledLoopRules.year.test(sp[0])) {
|
|
||||||
if (RegExp.$1 == 'S')
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-modeStrict');
|
|
||||||
else
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-modeRough');
|
|
||||||
loopRules += $.i18n.prop('ccn-i18n-datetime-loopRuleText-year')
|
|
||||||
.format(parseInt(RegExp.$2), datetimeInstance.toLocaleDateString(undefined, {timeZone: "UTC"}));
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.month.test(sp[0])) {
|
|
||||||
if (RegExp.$1 == 'S')
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-modeStrict');
|
|
||||||
else
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-modeRough');
|
|
||||||
|
|
||||||
var dayInMonth = ccn_datetime_GetDayInMonth(
|
|
||||||
datetimeInstance.getUTCFullYear(),
|
|
||||||
datetimeInstance.getUTCMonth() + 1,
|
|
||||||
datetimeInstance.getUTCDate());
|
|
||||||
switch(RegExp.$2) {
|
|
||||||
case 'A':
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-monthA')
|
|
||||||
.format(parseInt(RegExp.$3), dayInMonth[0]);
|
|
||||||
break;
|
|
||||||
case 'B':
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-monthB')
|
|
||||||
.format(parseInt(RegExp.$3), dayInMonth[1]);
|
|
||||||
break;
|
|
||||||
case 'C':
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-monthC')
|
|
||||||
.format(parseInt(RegExp.$3), dayInMonth[2], dayInMonth[3]);
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-monthD')
|
|
||||||
.format(parseInt(RegExp.$3), dayInMonth[4], dayInMonth[5]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.week.test(sp[0])) {
|
|
||||||
var weekOfDayCache = [];
|
|
||||||
for (var i = 0; i < 7; i++) {
|
|
||||||
if (RegExp.$1[i] == 'T')
|
|
||||||
weekOfDayCache.push(ccn_i18n_UniversalGetDayOfWeek(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-week')
|
|
||||||
.format(parseInt(RegExp.$2), weekOfDayCache.join(', '));
|
|
||||||
} else if (ccn_datetime_precompiledLoopRules.day.test(sp[0])) {
|
|
||||||
loopRules = $.i18n.prop('ccn-i18n-datetime-loopRuleText-day')
|
|
||||||
.format(parseInt(RegExp.$1));
|
|
||||||
} else return "";
|
|
||||||
|
|
||||||
|
|
||||||
if (ccn_datetime_precompiledLoopStopRules.infinity.test(sp[1])) {
|
|
||||||
loopStopRules = $.i18n.prop('ccn-i18n-datetime-loopStopRuleText-infinity');
|
|
||||||
} else if (ccn_datetime_precompiledLoopStopRules.datetime.test(sp[1])) {
|
|
||||||
loopStopRules = $.i18n.prop('ccn-i18n-datetime-loopStopRuleText-datetime')
|
|
||||||
.format(new Date(parseInt(RegExp.$1)).toLocaleDateString());
|
|
||||||
} else if (ccn_datetime_precompiledLoopStopRules.times.test(sp[1])) {
|
|
||||||
loopStopRules = $.i18n.prop('ccn-i18n-datetime-loopStopRuleText-times')
|
|
||||||
.format(parseInt(RegExp.$1));
|
|
||||||
} else return "";
|
|
||||||
|
|
||||||
return (loopRules + loopStopRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_LeapYearCountEx(endYear, includeThis, baseYear, includeBase) {
|
|
||||||
if (!includeThis) endYear--;
|
|
||||||
if (includeBase) baseYear--;
|
|
||||||
|
|
||||||
var endly = Math.floor(endYear / 4);
|
|
||||||
endly -= Math.floor(endYear / 100);
|
|
||||||
endly += Math.floor(endYear / 400);
|
|
||||||
|
|
||||||
var basely = Math.floor(baseYear / 4);
|
|
||||||
basely -= Math.floor(baseYear / 100);
|
|
||||||
basely += Math.floor(baseYear / 400);
|
|
||||||
|
|
||||||
return (endly - basely);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_DaysCount(year, month, day) {
|
|
||||||
var ly = ccn_datetime_LeapYearCountEx(year, false, 1, true);
|
|
||||||
var days = 365 * (year - 1);
|
|
||||||
days += ly;
|
|
||||||
|
|
||||||
for(var index = 1; index < month; index++)
|
|
||||||
days += ccn_datetime_monthDayCount[index - 1];
|
|
||||||
|
|
||||||
if (month > 2 && ccn_datetime_IsLeapYear(year))
|
|
||||||
days += 1;
|
|
||||||
|
|
||||||
days += day - 1;
|
|
||||||
return days;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_MonthsCount(year, month) {
|
|
||||||
return (year - 1) * 12 + (month - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_DayOfWeek(year, month, day) {
|
|
||||||
return ccn_datetime_DaysCount(year, month, day) % 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_GetDayInMonth(year, month, day) {
|
|
||||||
var days = ccn_datetime_monthDayCount[month - 1] + ((month == 2 && ccn_datetime_IsLeapYear(year)) ? 1 : 0);
|
|
||||||
var firstDayOfWeek = ccn_datetime_DayOfWeek(year, month, 1);
|
|
||||||
var dayOfWeek = (firstDayOfWeek + day - 1) % 7;
|
|
||||||
|
|
||||||
var dayForwards = day;
|
|
||||||
var dayBackwards = days - day + 1;
|
|
||||||
|
|
||||||
var weeksForward = Math.floor((dayForwards - 1) / 7) + 1;
|
|
||||||
var weeksBackwards = Math.floor((dayBackwards - 1) / 7) + 1;
|
|
||||||
|
|
||||||
return [dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_GetRemanagedDayInMonth(oldYear, oldMonth, oldDay, newYear, newMonth, isStrict) {
|
|
||||||
var ddata = ccn_datetime_GetDayInMonth(oldYear, oldMonth, oldDay);
|
|
||||||
var mdata = ccn_datetime_GetMonthWeekStatistics(newYear, newMonth);
|
|
||||||
var days = ccn_datetime_monthDayCount[newMonth - 1] + ((newMonth == 2 && ccn_datetime_IsLeapYear(year)) ? 1 : 0);
|
|
||||||
var firstDayOfWeek = ccn_datetime_DayOfWeek(newYear, newMonth, 1);
|
|
||||||
//var lastDayOfWeek = (firstDayOfWeek + days - 1) % 7;
|
|
||||||
|
|
||||||
if (isStrict) {
|
|
||||||
var methodA = ddata[0] > days ? undefined : ddata[0];
|
|
||||||
var methodB = ddata[1] > days ? undefined : (days - ddata[1] + 1);
|
|
||||||
} else {
|
|
||||||
var methodA = Math.min(ddata[0], days);
|
|
||||||
var methodB = days - Math.min(ddata[1], days) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var methodC = undefined;
|
|
||||||
if (ddata[2] <= mdata[ddata[3]] || !isStrict) {
|
|
||||||
var targetWeek = Math.min(ddata[2], mdata[ddata[3]]);
|
|
||||||
methodC = 1 + (targetWeek - 1) * 7 + ((ddata[3] + 7 - firstDayOfWeek) % 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
var methodD = undefined;
|
|
||||||
if (ddata[4] <= mdata[ddata[5]] || !isStrict) {
|
|
||||||
// convert to type c and calc
|
|
||||||
var targetWeek = mdata[ddata[5]] - Math.min(ddata[4], mdata[ddata[5]]) + 1;
|
|
||||||
methodD = 1 + (targetWeek - 1) * 7 + ((ddata[5] + 7 - firstDayOfWeek) % 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [methodA, methodB, methodC, methodD];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_GetMonthWeekStatistics(year, month) {
|
|
||||||
var days = ccn_datetime_monthDayCount[month - 1] + ((month == 2 && ccn_datetime_IsLeapYear(year)) ? 1 : 0);
|
|
||||||
var firstDayOfWeek = ccn_datetime_DayOfWeek(year, month, 1);
|
|
||||||
|
|
||||||
var result = [4, 4, 4, 4, 4, 4, 4];
|
|
||||||
var remain = days % 7;
|
|
||||||
var week = firstDayOfWeek;
|
|
||||||
while (remain > 0) {
|
|
||||||
result[week % 7] += 1;
|
|
||||||
week++;
|
|
||||||
remain--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetime_IsLeapYear(year) {
|
|
||||||
var isLeap = false;
|
|
||||||
if (year % 4 == 0) isLeap = true;
|
|
||||||
if (year % 100 == 0) isLeap = false;
|
|
||||||
if (year % 400 == 0) isLeap = true;
|
|
||||||
return isLeap;
|
|
||||||
}
|
|
||||||
@@ -1,522 +0,0 @@
|
|||||||
var ccn_datetimepicker_tabType = {
|
|
||||||
year: 0,
|
|
||||||
month: 1,
|
|
||||||
day: 2,
|
|
||||||
hour: 3,
|
|
||||||
minute: 4
|
|
||||||
};
|
|
||||||
|
|
||||||
var ccn_datetimepicker_dialPlateWidth = 200;
|
|
||||||
var ccn_datetimepicker_dialPlateRadius = ccn_datetimepicker_dialPlateWidth / 2;
|
|
||||||
var ccn_datetimepicker_dialPlateHourInnerPercent = 0.6;
|
|
||||||
var ccn_datetimepicker_dialPlateHourOutterPercent = 0.8;
|
|
||||||
var ccn_datetimepicker_dialPlateHourDistinguishPercent = 0.7;
|
|
||||||
var ccn_datetimepicker_dialPlateMinutePercent = 0.8;
|
|
||||||
var ccn_datetimepicker_dialPlateHourResolution = Math.PI * 2 / 12;
|
|
||||||
var ccn_datetimepicker_dialPlateMinuteResolution = Math.PI * 2 / 60;
|
|
||||||
|
|
||||||
var ccn_datetimepicker_mode = undefined;
|
|
||||||
var ccn_datetimepicker_isUTC = undefined;
|
|
||||||
var ccn_datetimepicker_pickerIndex = undefined;
|
|
||||||
|
|
||||||
var ccn_datetimepicker_enableMinuteDrag = false;
|
|
||||||
var ccn_datetimepicker_enableHourDrag = false;
|
|
||||||
|
|
||||||
var ccn_datetimepicker_internalDateTime = new Date();
|
|
||||||
var ccn_datetimepicker_displayCacheDateTime = new Date();
|
|
||||||
|
|
||||||
// ========================================= export func
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Insert() {
|
|
||||||
$('body').append(ccn_template_datetimepicker.render());
|
|
||||||
|
|
||||||
// bind size event and trigge once
|
|
||||||
$(window).resize(ccn_datetimepicker_RefreshSvg).resize();
|
|
||||||
|
|
||||||
// add data attr
|
|
||||||
for(var i = 0; i < 3; i++) {
|
|
||||||
for(var j = 0; j < 4; j++) {
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-table > div:nth-child({0}) > div:nth-child({1})'.format(i + 1, j + 1))
|
|
||||||
.attr('data', i * 4 + j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bind header event
|
|
||||||
$('header.pickerHeader > div').click(function() {
|
|
||||||
ccn_datetimepicker_SwitchTab(ccn_datetimepicker_Str2TabType($(this).attr('type')));
|
|
||||||
});
|
|
||||||
|
|
||||||
// bind button event
|
|
||||||
$('#ccn-datetimepiacker-panelYear-prevBtn').click(function() {
|
|
||||||
ccn_datetimepicker_PrevNextYear(true);
|
|
||||||
});
|
|
||||||
$('#ccn-datetimepiacker-panelYear-nextBtn').click(function() {
|
|
||||||
ccn_datetimepicker_PrevNextYear(false);
|
|
||||||
});
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-prevBtn').click(function() {
|
|
||||||
ccn_datetimepicker_PrevNextMonth(true);
|
|
||||||
});
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-nextBtn').click(function() {
|
|
||||||
ccn_datetimepicker_PrevNextMonth(false);
|
|
||||||
});
|
|
||||||
$('#ccn-datetimepiacker-panelDay-prevBtn').click(function() {
|
|
||||||
ccn_datetimepicker_PrevNextDay(true);
|
|
||||||
});
|
|
||||||
$('#ccn-datetimepiacker-panelDay-nextBtn').click(function() {
|
|
||||||
ccn_datetimepicker_PrevNextDay(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#ccn-datetimepiacker-panelYear-table > div > div').click(ccn_datetimepicker_ClickYear);
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-table > div > div').click(ccn_datetimepicker_ClickMonth);
|
|
||||||
$('#ccn-datetimepiacker-panelDay-table > div:nth-child(n+1) > div').click(ccn_datetimepicker_ClickDay);
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-panelHour')
|
|
||||||
.mousedown(ccn_datetimepicker_StartDragHour)
|
|
||||||
.mousemove(ccn_datetimepicker_DraggingHour)
|
|
||||||
.mouseup(ccn_datetimepicker_StopDragHour)
|
|
||||||
.on('touchstart', ccn_datetimepicker_StartDragHour)
|
|
||||||
.on('touchmove', ccn_datetimepicker_DraggingHour)
|
|
||||||
.on('touchend', ccn_datetimepicker_StopDragHour);
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-panelMinute')
|
|
||||||
.mousedown(ccn_datetimepicker_StartDragMinute)
|
|
||||||
.mousemove(ccn_datetimepicker_DraggingMinute)
|
|
||||||
.mouseup(ccn_datetimepicker_StopDragMinute)
|
|
||||||
.on('touchstart', ccn_datetimepicker_StartDragMinute)
|
|
||||||
.on('touchmove', ccn_datetimepicker_DraggingMinute)
|
|
||||||
.on('touchend', ccn_datetimepicker_StopDragMinute);
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-btnConfirm').click(ccn_datetimepicker_Confirm);
|
|
||||||
$('#ccn-datetimepicker-btnCancel').click(ccn_datetimepicker_Cancel);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Modal(mode, pickerIndex, isUTC) {
|
|
||||||
ccn_datetimepicker_mode = mode;
|
|
||||||
ccn_datetimepicker_isUTC = isUTC;
|
|
||||||
ccn_datetimepicker_pickerIndex = pickerIndex;
|
|
||||||
|
|
||||||
ccn_datetimepicker_internalDateTime = ccn_datetimepicker_Get(pickerIndex, false);
|
|
||||||
|
|
||||||
$('header.pickerHeader > div').hide();
|
|
||||||
switch(mode) {
|
|
||||||
case ccn_datetimepicker_tabType.minute:
|
|
||||||
$('header.pickerHeader > div[type=minute]').show();
|
|
||||||
case ccn_datetimepicker_tabType.hour:
|
|
||||||
$('header.pickerHeader > div[type=hour]').show();
|
|
||||||
case ccn_datetimepicker_tabType.day:
|
|
||||||
$('header.pickerHeader > div[type=day]').show();
|
|
||||||
case ccn_datetimepicker_tabType.month:
|
|
||||||
$('header.pickerHeader > div[type=month]').show();
|
|
||||||
case ccn_datetimepicker_tabType.year:
|
|
||||||
$('header.pickerHeader > div[type=year]').show();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-modal').addClass('is-active');
|
|
||||||
ccn_datetimepicker_SwitchTab(mode); // this call is set in there by design. if you don't show the dialog, the call of svg resize will fail.
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Confirm() {
|
|
||||||
// update and call callback func
|
|
||||||
ccn_datetimepicker_Set(
|
|
||||||
ccn_datetimepicker_pickerIndex,
|
|
||||||
ccn_datetimepicker_internalDateTime,
|
|
||||||
ccn_datetimepicker_isUTC,
|
|
||||||
ccn_datetimepicker_mode
|
|
||||||
);
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-modal').removeClass('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Cancel() {
|
|
||||||
$('#ccn-datetimepicker-modal').removeClass('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================= internal func
|
|
||||||
|
|
||||||
function ccn_datetimepicker_OnSvgResize(ele) {
|
|
||||||
var scale = 200 / Math.min(ele.width(), ele.height());
|
|
||||||
ele.css('font-size', scale + 'em');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_SwitchTab(newTab) {
|
|
||||||
$('div.pickerContainer > *').hide();
|
|
||||||
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.setTime(ccn_datetimepicker_internalDateTime.getTime());
|
|
||||||
ccn_datetimepicker_RefreshDisplay(newTab);
|
|
||||||
|
|
||||||
switch(newTab) {
|
|
||||||
case ccn_datetimepicker_tabType.year:
|
|
||||||
$('#ccn-datetimepicker-panelYear').show();
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.month:
|
|
||||||
$('#ccn-datetimepicker-panelMonth').show();
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.day:
|
|
||||||
$('#ccn-datetimepicker-panelDay').show();
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.hour:
|
|
||||||
$('#ccn-datetimepicker-panelHour').show();
|
|
||||||
ccn_datetimepicker_RefreshSvg(); // immediately trigger once svg resize
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.minute:
|
|
||||||
$('#ccn-datetimepicker-panelMinute').show();
|
|
||||||
ccn_datetimepicker_RefreshSvg(); // immediately trigger once svg resize
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_RefreshDisplay(tab) {
|
|
||||||
// header should be refreshed entirely
|
|
||||||
$('#ccn-datetimepicker-datetime-year').text(ccn_datetimepicker_internalDateTime.getFullYear());
|
|
||||||
$('#ccn-datetimepicker-datetime-month').text(ccn_datetimepicker_internalDateTime.getMonth() + 1);
|
|
||||||
$('#ccn-datetimepicker-datetime-day').text(ccn_datetimepicker_internalDateTime.getDate());
|
|
||||||
$('#ccn-datetimepicker-datetime-hour').text(ccn_datetimepicker_internalDateTime.getHours());
|
|
||||||
$('#ccn-datetimepicker-datetime-minute').text(ccn_datetimepicker_internalDateTime.getMinutes());
|
|
||||||
|
|
||||||
// refresh tab according to specific `tab`
|
|
||||||
switch(tab) {
|
|
||||||
case ccn_datetimepicker_tabType.year:
|
|
||||||
var startYear = Math.floor((ccn_datetimepicker_displayCacheDateTime.getFullYear() - ccn_datetime_MIN_YEAR) / 12) * 12 + ccn_datetime_MIN_YEAR;
|
|
||||||
var counter = startYear;
|
|
||||||
for(var i = 0; i < 3; i++) {
|
|
||||||
for(var j = 0; j < 4; j++, counter++) {
|
|
||||||
var ele = $('#ccn-datetimepiacker-panelYear-table > div:nth-child({0}) > div:nth-child({1})'.format(i + 1, j + 1));
|
|
||||||
if (counter < ccn_datetime_MAX_YEAR) {
|
|
||||||
ele.attr('data', counter)
|
|
||||||
.text(counter);
|
|
||||||
} else {
|
|
||||||
ele.attr('data', '')
|
|
||||||
.html(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter == ccn_datetimepicker_internalDateTime.getFullYear()) ele.attr('picked', 'true');
|
|
||||||
else ele.attr('picked', 'false');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#ccn-datetimepiacker-panelYear-title')
|
|
||||||
.text('{0} - {1}'.format(startYear, startYear + 12 < ccn_datetime_MAX_YEAR ? startYear + 12 : ccn_datetime_MAX_YEAR));
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.month:
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-table > div > div').attr('picked', 'false');
|
|
||||||
if (ccn_datetimepicker_internalDateTime.getFullYear() == ccn_datetimepicker_displayCacheDateTime.getFullYear()) {
|
|
||||||
var month = ccn_datetimepicker_internalDateTime.getMonth();
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-table > div:nth-child({0}) > div:nth-child({1})'.format(Math.floor(month / 4) + 1, (month % 4) + 1))
|
|
||||||
.attr('picked', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#ccn-datetimepiacker-panelMonth-title')
|
|
||||||
.text(ccn_datetimepicker_displayCacheDateTime.getFullYear());
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.day:
|
|
||||||
var gottenYear = ccn_datetimepicker_displayCacheDateTime.getFullYear();
|
|
||||||
var gottenMonth = ccn_datetimepicker_displayCacheDateTime.getMonth() + 1;
|
|
||||||
var counter = -ccn_datetime_DayOfWeek(gottenYear, gottenMonth, 1);
|
|
||||||
var days = ccn_datetime_monthDayCount[gottenMonth - 1] + ((gottenMonth == 2 && ccn_datetime_IsLeapYear(gottenYear)) ? 1 : 0);
|
|
||||||
for(var i = 0; i < 6; i++) {
|
|
||||||
for(var j = 0; j < 7; j++, counter++) {
|
|
||||||
var ele = $('#ccn-datetimepiacker-panelDay-table > div:nth-child({0}) > div:nth-child({1})'.format(i + 2, j + 1));
|
|
||||||
if (counter < 0 || counter >= days) ele.attr('data', '').html(' ');
|
|
||||||
else ele.attr('data', counter + 1).text(counter + 1);
|
|
||||||
|
|
||||||
if (counter + 1 == ccn_datetimepicker_internalDateTime.getDate()) ele.attr('picked', 'true');
|
|
||||||
else ele.attr('picked', 'false');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#ccn-datetimepiacker-panelDay-title')
|
|
||||||
.text('{0} - {1}'.format(
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getFullYear(),
|
|
||||||
ccn_i18n_UniversalGetMonth(ccn_datetimepicker_displayCacheDateTime.getMonth())
|
|
||||||
));
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.hour:
|
|
||||||
var gottenHour = ccn_datetimepicker_displayCacheDateTime.getHours();
|
|
||||||
var newX = Math.cos((3 - gottenHour) * Math.PI * 2 / 12);
|
|
||||||
var newY = Math.sin((3 - gottenHour) * Math.PI * 2 / 12);
|
|
||||||
var radius = ccn_datetimepicker_dialPlateRadius * (gottenHour < 12 ? ccn_datetimepicker_dialPlateHourOutterPercent : ccn_datetimepicker_dialPlateHourInnerPercent);
|
|
||||||
newX = newX * radius + ccn_datetimepicker_dialPlateRadius;
|
|
||||||
newY = (-newY * radius) + ccn_datetimepicker_dialPlateRadius;
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-panelHour > line')
|
|
||||||
.attr('x2', newX)
|
|
||||||
.attr('y2', newY);
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-panelHour > circle[type=symbol]')
|
|
||||||
.attr('cx', newX)
|
|
||||||
.attr('cy', newY);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ccn_datetimepicker_tabType.minute:
|
|
||||||
var gottenMinute = ccn_datetimepicker_displayCacheDateTime.getMinutes();
|
|
||||||
var newX = Math.cos((15 - gottenMinute) * Math.PI * 2 / 60);
|
|
||||||
var newY = Math.sin((15 - gottenMinute) * Math.PI * 2 / 60);
|
|
||||||
var radius = ccn_datetimepicker_dialPlateRadius * ccn_datetimepicker_dialPlateMinutePercent;
|
|
||||||
newX = newX * radius + ccn_datetimepicker_dialPlateRadius;
|
|
||||||
newY = (-newY * radius) + ccn_datetimepicker_dialPlateRadius;
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-panelMinute > line')
|
|
||||||
.attr('x2', newX)
|
|
||||||
.attr('y2', newY);
|
|
||||||
|
|
||||||
$('#ccn-datetimepicker-panelMinute > circle[type=symbol]')
|
|
||||||
.attr('cx', newX)
|
|
||||||
.attr('cy', newY);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_RefreshSvg() {
|
|
||||||
// svg resize only can be called when the svg is showing.
|
|
||||||
// so call this func in window resize event or
|
|
||||||
// displaying svg.
|
|
||||||
$('div.pickerContainer > svg').each(function() {
|
|
||||||
ccn_datetimepicker_OnSvgResize($(this));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Str2TabType(strl) {
|
|
||||||
switch(strl) {
|
|
||||||
case 'year':
|
|
||||||
return ccn_datetimepicker_tabType.year
|
|
||||||
case 'month':
|
|
||||||
return ccn_datetimepicker_tabType.month
|
|
||||||
case 'day':
|
|
||||||
return ccn_datetimepicker_tabType.day
|
|
||||||
case 'hour':
|
|
||||||
return ccn_datetimepicker_tabType.hour
|
|
||||||
case 'minute':
|
|
||||||
return ccn_datetimepicker_tabType.minute
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_GetUniformedXY(mouseOrTouchEvent, elements) {
|
|
||||||
var offset = {
|
|
||||||
left: elements.offset().left,
|
|
||||||
top: elements.offset().top,
|
|
||||||
halfWidth: elements.width() / 2,
|
|
||||||
halfHeight: elements.height() / 2,
|
|
||||||
halfSquareWidthHeight: Math.min(elements.width(), elements.height()) / 2
|
|
||||||
}
|
|
||||||
if(typeof(mouseOrTouchEvent.pageX) != 'undefined' && typeof(mouseOrTouchEvent.pageY) != 'undefined') {
|
|
||||||
offset.realX = mouseOrTouchEvent.pageX;
|
|
||||||
offset.realY = mouseOrTouchEvent.pageY;
|
|
||||||
} else if(typeof(mouseOrTouchEvent.targetTouches) != 'undefined' && mouseOrTouchEvent.targetTouches.length >= 1) {
|
|
||||||
offset.realX = mouseOrTouchEvent.targetTouches[0].pageX;
|
|
||||||
offset.realY = mouseOrTouchEvent.targetTouches[0].pageY;
|
|
||||||
} else {
|
|
||||||
offset.realX = 0;
|
|
||||||
offset.realY = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var _x = (offset.realX - offset.left - offset.halfWidth) / offset.halfSquareWidthHeight * ccn_datetimepicker_dialPlateRadius;
|
|
||||||
var _y = -((offset.realY - offset.top - offset.halfHeight) / offset.halfSquareWidthHeight * ccn_datetimepicker_dialPlateRadius);
|
|
||||||
|
|
||||||
return {x: _x, y: _y};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_PrevNextYear(isPrev) {
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.setFullYear(
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getFullYear() + (isPrev ? -12 : 12));
|
|
||||||
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_displayCacheDateTime);
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.year);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_PrevNextMonth(isPrev) {
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.setFullYear(
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getFullYear() + (isPrev ? -1 : 1));
|
|
||||||
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_displayCacheDateTime);
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.month);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_PrevNextDay(isPrev) {
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.setMonth(
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getMonth() + (isPrev ? -1 : 1));
|
|
||||||
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_displayCacheDateTime);
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_ClickYear() {
|
|
||||||
var ele = $(this);
|
|
||||||
if (ele.attr('data') == '') return;
|
|
||||||
|
|
||||||
ccn_datetimepicker_internalDateTime.setFullYear(parseInt(ele.attr('data')));
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_internalDateTime);
|
|
||||||
|
|
||||||
if (ccn_datetimepicker_mode != ccn_datetimepicker_tabType.year)
|
|
||||||
ccn_datetimepicker_SwitchTab(ccn_datetimepicker_tabType.month);
|
|
||||||
else
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.year);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_ClickMonth() {
|
|
||||||
var ele = $(this);
|
|
||||||
if (ele.attr('data') == '') return;
|
|
||||||
|
|
||||||
ccn_datetimepicker_internalDateTime.setFullYear(
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getFullYear(),
|
|
||||||
parseInt(ele.attr('data'))
|
|
||||||
);
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_internalDateTime);
|
|
||||||
|
|
||||||
if (ccn_datetimepicker_mode != ccn_datetimepicker_tabType.month)
|
|
||||||
ccn_datetimepicker_SwitchTab(ccn_datetimepicker_tabType.day);
|
|
||||||
else
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.month);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_ClickDay() {
|
|
||||||
var ele = $(this);
|
|
||||||
if (ele.attr('data') == '') return;
|
|
||||||
|
|
||||||
ccn_datetimepicker_internalDateTime.setFullYear(
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getFullYear(),
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.getMonth(),
|
|
||||||
parseInt(ele.attr('data'))
|
|
||||||
);
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_internalDateTime);
|
|
||||||
|
|
||||||
if (ccn_datetimepicker_mode != ccn_datetimepicker_tabType.day)
|
|
||||||
ccn_datetimepicker_SwitchTab(ccn_datetimepicker_tabType.hour);
|
|
||||||
else
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_datetimepicker_StartDragHour() { ccn_datetimepicker_enableHourDrag = true; }
|
|
||||||
function ccn_datetimepicker_DraggingHour(e) {
|
|
||||||
if (!ccn_datetimepicker_enableHourDrag) return;
|
|
||||||
|
|
||||||
var offset = ccn_datetimepicker_GetUniformedXY(e, $('#ccn-datetimepicker-panelHour'));
|
|
||||||
var x = offset.x;
|
|
||||||
var y = offset.y;
|
|
||||||
|
|
||||||
var distance = Math.sqrt(x * x + y * y);
|
|
||||||
var angle = Math.acos(x / distance);
|
|
||||||
if (y < 0) angle = Math.PI * 2 - angle; // correct negative y axis angle
|
|
||||||
|
|
||||||
angle += (ccn_datetimepicker_dialPlateHourResolution / 2); // correct offset
|
|
||||||
if (angle > Math.PI * 2)
|
|
||||||
angle -= Math.PI * 2;
|
|
||||||
|
|
||||||
var number = Math.floor(angle / ccn_datetimepicker_dialPlateHourResolution);
|
|
||||||
if (number >= 12) number = 11; // prevent unexpected result at the edge.
|
|
||||||
number = (15 - number) % 12;
|
|
||||||
if (distance < ccn_datetimepicker_dialPlateRadius * ccn_datetimepicker_dialPlateHourDistinguishPercent)
|
|
||||||
number += 12;
|
|
||||||
|
|
||||||
// judge
|
|
||||||
if (ccn_datetimepicker_displayCacheDateTime.getHours() != number) {
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.setHours(number);
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.hour);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
function ccn_datetimepicker_StopDragHour() {
|
|
||||||
ccn_datetimepicker_enableHourDrag = false;
|
|
||||||
|
|
||||||
ccn_datetimepicker_internalDateTime.setHours(ccn_datetimepicker_displayCacheDateTime.getHours());
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_internalDateTime);
|
|
||||||
|
|
||||||
if (ccn_datetimepicker_mode != ccn_datetimepicker_tabType.hour)
|
|
||||||
ccn_datetimepicker_SwitchTab(ccn_datetimepicker_tabType.minute);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_datetimepicker_StartDragMinute() { ccn_datetimepicker_enableMinuteDrag = true; }
|
|
||||||
function ccn_datetimepicker_DraggingMinute(e) {
|
|
||||||
if (!ccn_datetimepicker_enableMinuteDrag) return;
|
|
||||||
|
|
||||||
var offset = ccn_datetimepicker_GetUniformedXY(e, $('#ccn-datetimepicker-panelMinute'));
|
|
||||||
var x = offset.x;
|
|
||||||
var y = offset.y;
|
|
||||||
|
|
||||||
var distance = Math.sqrt(x * x + y * y);
|
|
||||||
var angle = Math.acos(x / distance);
|
|
||||||
if (y < 0) angle = Math.PI * 2 - angle; // correct negative y axis angle
|
|
||||||
|
|
||||||
angle += (ccn_datetimepicker_dialPlateMinuteResolution / 2); // correct offset
|
|
||||||
if (angle > Math.PI * 2)
|
|
||||||
angle -= Math.PI * 2;
|
|
||||||
|
|
||||||
var number = Math.floor(angle / ccn_datetimepicker_dialPlateMinuteResolution);
|
|
||||||
if (number >= 60) number = 59; // prevent unexpected result at the edge.
|
|
||||||
number = (75 - number) % 60;
|
|
||||||
|
|
||||||
// judge
|
|
||||||
if (ccn_datetimepicker_displayCacheDateTime.getMinutes() != number) {
|
|
||||||
ccn_datetimepicker_displayCacheDateTime.setMinutes(number);
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.minute);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
function ccn_datetimepicker_StopDragMinute() {
|
|
||||||
ccn_datetimepicker_enableMinuteDrag = false;
|
|
||||||
|
|
||||||
ccn_datetimepicker_internalDateTime.setMinutes(ccn_datetimepicker_displayCacheDateTime.getMinutes());
|
|
||||||
ccn_datetimepicker_ClampDateTime(ccn_datetimepicker_internalDateTime);
|
|
||||||
|
|
||||||
// no page need to go to
|
|
||||||
// but we need refresh current page
|
|
||||||
ccn_datetimepicker_RefreshDisplay(ccn_datetimepicker_tabType.minute);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_datetimepicker_ClampDateTime(dateObj) {
|
|
||||||
if (dateObj < ccn_datetime_MIN_DATETIME)
|
|
||||||
dateObj.setTime(ccn_datetime_MIN_DATETIME.getTime());
|
|
||||||
if (dateObj >= ccn_datetime_MAX_DATETIME)
|
|
||||||
dateObj.setTime(ccn_datetime_MAX_DATETIME.getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================================== universal function
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Set(pickerIndex, dt, isUTC, mode) {
|
|
||||||
var ele = $('[datetimepicker=' + pickerIndex + ']');
|
|
||||||
while(true) {
|
|
||||||
if (mode < ccn_datetimepicker_tabType.year) break;
|
|
||||||
ele.attr('datetimepicker-year', isUTC ? dt.getUTCFullYear() : dt.getFullYear());
|
|
||||||
if (mode < ccn_datetimepicker_tabType.month) break;
|
|
||||||
ele.attr('datetimepicker-month', (isUTC ? dt.getUTCMonth() : dt.getMonth()) + 1);
|
|
||||||
if (mode < ccn_datetimepicker_tabType.day) break;
|
|
||||||
ele.attr('datetimepicker-day', isUTC ? dt.getUTCDate() : dt.getDate());
|
|
||||||
if (mode < ccn_datetimepicker_tabType.hour) break;
|
|
||||||
ele.attr('datetimepicker-hour', isUTC ? dt.getUTCHours() : dt.getHours());
|
|
||||||
if (mode < ccn_datetimepicker_tabType.minute) break;
|
|
||||||
ele.attr('datetimepicker-minute', isUTC ? dt.getUTCMinutes() : dt.getMinutes());
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(ele.prop('funcs')) != 'undefined' && typeof(ele.prop('funcs').callback) == 'function')
|
|
||||||
ele.prop('funcs').callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_datetimepicker_Get(pickerIndex, isUTC) {
|
|
||||||
var ele = $('[datetimepicker=' + pickerIndex + ']');
|
|
||||||
year = ele.attr('datetimepicker-year');
|
|
||||||
month = ele.attr('datetimepicker-month');
|
|
||||||
day = ele.attr('datetimepicker-day');
|
|
||||||
hour = ele.attr('datetimepicker-hour');
|
|
||||||
minute = ele.attr('datetimepicker-minute');
|
|
||||||
if (IsUndefinedOrEmpty(year)) year = ccn_datetime_MIN_YEAR;
|
|
||||||
if (IsUndefinedOrEmpty(month)) month = 1;
|
|
||||||
if (IsUndefinedOrEmpty(day)) day = 1;
|
|
||||||
if (IsUndefinedOrEmpty(hour)) hour = 0;
|
|
||||||
if (IsUndefinedOrEmpty(minute)) minute = 0;
|
|
||||||
|
|
||||||
if (isUTC) return new Date(Date.UTC(year, parseInt(month) - 1, day, hour, minute, 0, 0));
|
|
||||||
else return new Date(year, parseInt(month) - 1, day, hour, minute, 0, 0);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
function ccn_headerNav_Insert() {
|
|
||||||
$('body').prepend(ccn_template_headerNav.render());
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_headerNav_LoggedRefresh() {
|
|
||||||
if (ccn_api_common_tokenValid()) {
|
|
||||||
// logged, show all nav button and logout button
|
|
||||||
$("#ccn-header-nav-home").show();
|
|
||||||
$("#ccn-header-nav-collection").show();
|
|
||||||
$("#ccn-header-nav-calendar").show();
|
|
||||||
$("#ccn-header-nav-todo").show();
|
|
||||||
$("#ccn-header-nav-admin").show();
|
|
||||||
|
|
||||||
$("#ccn-header-user-login").hide();
|
|
||||||
$("#ccn-header-user-logout").show();
|
|
||||||
} else {
|
|
||||||
$("#ccn-header-nav-home").show();
|
|
||||||
$("#ccn-header-nav-collection").hide();
|
|
||||||
$("#ccn-header-nav-calendar").hide();
|
|
||||||
$("#ccn-header-nav-todo").hide();
|
|
||||||
$("#ccn-header-nav-admin").hide();
|
|
||||||
|
|
||||||
$("#ccn-header-user-login").show();
|
|
||||||
$("#ccn-header-user-logout").hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bind language process and internal process function such as logout and expand menu
|
|
||||||
function ccn_headerNav_BindEvents() {
|
|
||||||
// bind function
|
|
||||||
$("#ccn-header-language > *").each(function(){
|
|
||||||
$(this).click(function(){
|
|
||||||
ccn_i18n_ChangeLanguage($(this).attr("language"));
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// bind logout
|
|
||||||
$("#ccn-header-user-logout").click(function() {
|
|
||||||
if (ccn_api_common_logout()) {
|
|
||||||
// ok, logout
|
|
||||||
// jump into home page again
|
|
||||||
window.location.href = '/web/home';
|
|
||||||
return;
|
|
||||||
|
|
||||||
} else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-logout"));
|
|
||||||
});
|
|
||||||
|
|
||||||
// bind burger menu
|
|
||||||
// copy from bulma website
|
|
||||||
// Check for click events on the navbar burger icon
|
|
||||||
$(".navbar-burger").click(function() {
|
|
||||||
|
|
||||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
|
||||||
$(".navbar-burger").toggleClass("is-active");
|
|
||||||
$(".navbar-menu").toggleClass("is-active");
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
var ccn_i18n_i18nSupported = ['en-US', 'zh-CN'];
|
|
||||||
var ccn_i18n_currentLanguage = 'en-US';
|
|
||||||
var ccn_pages_enumPages = {
|
|
||||||
home : 0,
|
|
||||||
calendar: 1,
|
|
||||||
todo: 2,
|
|
||||||
admin: 3,
|
|
||||||
login: 4,
|
|
||||||
collection: 5,
|
|
||||||
event: 6
|
|
||||||
};
|
|
||||||
var ccn_pages_currentPage = ccn_pages_enumPages.home;
|
|
||||||
|
|
||||||
// judge current language
|
|
||||||
ccn_i18n_currentLanguage = ccn_localstorageAssist_Get('ccn-i18n', 'en-US');
|
|
||||||
if (ccn_i18n_i18nSupported.indexOf(ccn_i18n_currentLanguage) == -1){
|
|
||||||
ccn_localstorageAssist_Set('ccn-i18n', 'en-US');
|
|
||||||
ccn_i18n_currentLanguage = 'en-US';
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_i18n_ChangeLanguage(newLang) {
|
|
||||||
if (ccn_i18n_i18nSupported.indexOf(newLang) == -1) return false;
|
|
||||||
ccn_i18n_currentLanguage = newLang;
|
|
||||||
ccn_localstorageAssist_Set('ccn-i18n', newLang);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_i18n_LoadLanguage() {
|
|
||||||
$.i18n.properties({
|
|
||||||
name: 'strings_' + ccn_i18n_currentLanguage,
|
|
||||||
path: '/static/i18n/',
|
|
||||||
encoding: 'utf-8',
|
|
||||||
mode: 'map',
|
|
||||||
async: false,
|
|
||||||
cache: false,
|
|
||||||
language: ccn_i18n_currentLanguage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_i18n_ApplyLanguage() {
|
|
||||||
//set usual block
|
|
||||||
var cache = $("[i18n-name]");
|
|
||||||
cache.each(function() {
|
|
||||||
$(this).html($.i18n.prop($(this).attr('i18n-name')));
|
|
||||||
});
|
|
||||||
|
|
||||||
//set unusual block
|
|
||||||
//set title
|
|
||||||
switch(ccn_pages_currentPage) {
|
|
||||||
case ccn_pages_enumPages.home:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-home'));
|
|
||||||
break;
|
|
||||||
case ccn_pages_enumPages.calendar:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-calendar'));
|
|
||||||
break;
|
|
||||||
case ccn_pages_enumPages.todo:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-todo'));
|
|
||||||
break;
|
|
||||||
case ccn_pages_enumPages.admin:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-admin'));
|
|
||||||
break;
|
|
||||||
case ccn_pages_enumPages.login:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-login'));
|
|
||||||
break;
|
|
||||||
case ccn_pages_enumPages.collection:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-collection'));
|
|
||||||
break;
|
|
||||||
case ccn_pages_enumPages.event:
|
|
||||||
$('#ccn-pageName').html($.i18n.prop('ccn-i18n-pageName-event'));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_i18n_ApplyLanguage2Content(ctx) {
|
|
||||||
ctx.find("[i18n-name]").each(function() {
|
|
||||||
$(this).html($.i18n.prop($(this).attr('i18n-name')));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: month is zero based
|
|
||||||
function ccn_i18n_UniversalGetMonth(month) {
|
|
||||||
return $.i18n.prop('ccn-i18n-universal-month-' + (month + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: day of week is zero based
|
|
||||||
function ccn_i18n_UniversalGetDayOfWeek(dayOfWeek) {
|
|
||||||
return $.i18n.prop('ccn-i18n-universal-week-' + (dayOfWeek + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
function ccn_localstorageAssist_Get(index, defaultValue) {
|
|
||||||
var cache = localStorage.getItem(index);
|
|
||||||
if (cache == null) {
|
|
||||||
ccn_localstorageAssist_Set(index, defaultValue);
|
|
||||||
return defaultValue;
|
|
||||||
} else return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_localstorageAssist_Set(index, value) {
|
|
||||||
localStorage.setItem(index, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================== seperated data getter setter
|
|
||||||
|
|
||||||
function ccn_localstorageAssist_GetApiToken() {
|
|
||||||
return ccn_localstorageAssist_Get('ccn-token', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_localstorageAssist_SetApiToken(value) {
|
|
||||||
ccn_localstorageAssist_Set('ccn-token', value);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
function ccn_messagebox_Insert() {
|
|
||||||
$('body').append(ccn_template_messagebox.render());
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_messagebox_Show(/*title,*/ info) {
|
|
||||||
//$('#ccn-messagebox-title').text(title);
|
|
||||||
$('#ccn-messagebox-body').text(info);
|
|
||||||
|
|
||||||
$('#ccn-messagebox-modal').addClass('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_messagebox_BindEvent() {
|
|
||||||
$('#ccn-messagebox-btnClose').click(ccn_messagebox_Hide);
|
|
||||||
$('#ccn-messagebox-btnConfirm').click(ccn_messagebox_Hide);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_messagebox_Hide() {
|
|
||||||
$('#ccn-messagebox-modal').removeClass('is-active');
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
var ccn_admin_userListCache = [];
|
|
||||||
var ccn_admin_tokenListCache = [];
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.admin;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// bind tab control switcher and set current tab
|
|
||||||
$("#tabcontrol-tab-1-1").click(function(){
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 1);
|
|
||||||
});
|
|
||||||
$("#tabcontrol-tab-1-2").click(function(){
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 2);
|
|
||||||
});
|
|
||||||
$("#tabcontrol-tab-1-3").click(function(){
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 3);
|
|
||||||
});
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 1);
|
|
||||||
|
|
||||||
// load user tab according to admin status
|
|
||||||
if(!ccn_api_profile_isAdmin())
|
|
||||||
$('#tabcontrol-tab-1-3').hide();
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$('#ccn-admin-profile-btnChangePassword').click(ccn_admin_profile_ChangePassword);
|
|
||||||
$('#ccn-admin-tokenList-btnRefresh').click(ccn_admin_tokenList_Refresh);
|
|
||||||
$('#ccn-admin-userList-btnAdd').click(ccn_admin_userList_Add);
|
|
||||||
$('#ccn-admin-userList-btnRefresh').click(ccn_admin_userList_Refresh);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ================== profile
|
|
||||||
|
|
||||||
function ccn_admin_profile_ChangePassword() {
|
|
||||||
var newpassword = $('#ccn-admin-profile-inputPassword').val();
|
|
||||||
if (newpassword == "") return;
|
|
||||||
|
|
||||||
var result = ccn_api_profile_changePassword(newpassword);
|
|
||||||
if(result) {
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-success"));
|
|
||||||
$('#ccn-admin-profile-inputPassword').val('');
|
|
||||||
} else
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-update"));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================== token
|
|
||||||
|
|
||||||
function ccn_admin_tokenList_Refresh() {
|
|
||||||
ccn_admin_tokenListCache = new Array();
|
|
||||||
var listDOM = $('#ccn-admin-tokenList');
|
|
||||||
listDOM.empty();
|
|
||||||
|
|
||||||
var renderdata = {
|
|
||||||
uuid: undefined,
|
|
||||||
isMe: undefined,
|
|
||||||
ua: undefined,
|
|
||||||
ip: undefined,
|
|
||||||
expireOn: undefined
|
|
||||||
}
|
|
||||||
var gottenDateTime = new Date();
|
|
||||||
|
|
||||||
var result = ccn_api_profile_getToken();
|
|
||||||
if(typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
var item = result[index];
|
|
||||||
renderdata.uuid = item[1];
|
|
||||||
renderdata.isMe = ccn_localstorageAssist_GetApiToken() == item[1];
|
|
||||||
renderdata.ua = item[3];
|
|
||||||
renderdata.ip = item[4];
|
|
||||||
gottenDateTime.setTime(item[2] * 1000);
|
|
||||||
renderdata.expireOn = gottenDateTime.toLocaleString();
|
|
||||||
|
|
||||||
listDOM.append(ccn_template_tokenItem.render(renderdata));
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
var uuid = renderdata.uuid;
|
|
||||||
$("#ccn-tokenItem-btnLogout-" + uuid).click(ccn_admin_tokenList_ItemDelete);
|
|
||||||
|
|
||||||
// add into cache
|
|
||||||
ccn_admin_tokenListCache[uuid] = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
ccn_i18n_ApplyLanguage2Content(listDOM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_tokenList_ItemDelete() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
var result = ccn_api_profile_deleteToken(uuid);
|
|
||||||
|
|
||||||
if(!result) {
|
|
||||||
// fail
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-delete"));
|
|
||||||
} else {
|
|
||||||
// remove body
|
|
||||||
$("#ccn-tokenItem-" + uuid).remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================== user list
|
|
||||||
|
|
||||||
function ccn_admin_userList_RefreshCacheList() {
|
|
||||||
ccn_admin_userListCache = new Array();
|
|
||||||
|
|
||||||
var result = ccn_api_admin_get();
|
|
||||||
if(typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
ccn_admin_userListCache[index] = result[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_RenderItem(item, index, listDOM) {
|
|
||||||
var renderdata = {
|
|
||||||
uuid: index, // use index for uuid. there are no uuid for user
|
|
||||||
username: item[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// render
|
|
||||||
listDOM.append(ccn_template_userItem.render(renderdata));
|
|
||||||
|
|
||||||
// set mode
|
|
||||||
var uuid = index;
|
|
||||||
ccn_admin_userList_ChangeDisplayMode(uuid, false, item[1])
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$("#ccn-userItem-btnEdit-" + uuid).click(ccn_admin_userList_ItemEdit);
|
|
||||||
$("#ccn-userItem-btnDelete-" + uuid).click(ccn_admin_userList_ItemDelete);
|
|
||||||
$("#ccn-userItem-btnUpdate-" + uuid).click(ccn_admin_userList_ItemUpdate);
|
|
||||||
$("#ccn-userItem-btnCancelUpdate-" + uuid).click(ccn_admin_userList_ItemCancelUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_RenderCacheList() {
|
|
||||||
$('#ccn-admin-userList').empty();
|
|
||||||
|
|
||||||
var listDOM = $('#ccn-admin-userList');
|
|
||||||
for(var index in ccn_admin_userListCache) {
|
|
||||||
ccn_admin_userList_RenderItem(
|
|
||||||
ccn_admin_userListCache[index],
|
|
||||||
index,
|
|
||||||
listDOM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ccn_i18n_ApplyLanguage2Content(listDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_ChangeDisplayMode(uuid, isEdit, isAdmin) {
|
|
||||||
if (typeof(isAdmin) != 'undefined') {
|
|
||||||
if (isAdmin)
|
|
||||||
$("#ccn-userItem-iconIsAdmin-" + uuid).show();
|
|
||||||
else
|
|
||||||
$("#ccn-userItem-iconIsAdmin-" + uuid).hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(isEdit) != 'undefined') {
|
|
||||||
if (isEdit) {
|
|
||||||
$("#ccn-userItem-btnEdit-" + uuid).hide();
|
|
||||||
$("#ccn-userItem-btnDelete-" + uuid).hide();
|
|
||||||
$("#ccn-userItem-btnUpdate-" + uuid).show();
|
|
||||||
$("#ccn-userItem-btnCancelUpdate-" + uuid).show();
|
|
||||||
|
|
||||||
$("#ccn-userItem-boxPassword-" + uuid).show();
|
|
||||||
$("#ccn-userItem-boxIsAdmin-" + uuid).show();
|
|
||||||
} else {
|
|
||||||
$("#ccn-userItem-btnEdit-" + uuid).show();
|
|
||||||
$("#ccn-userItem-btnDelete-" + uuid).show();
|
|
||||||
$("#ccn-userItem-btnUpdate-" + uuid).hide();
|
|
||||||
$("#ccn-userItem-btnCancelUpdate-" + uuid).hide();
|
|
||||||
|
|
||||||
$("#ccn-userItem-boxPassword-" + uuid).hide();
|
|
||||||
$("#ccn-userItem-boxIsAdmin-" + uuid).hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_Refresh() {
|
|
||||||
// refresh and render once
|
|
||||||
ccn_admin_userList_RefreshCacheList();
|
|
||||||
ccn_admin_userList_RenderCacheList();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_Add() {
|
|
||||||
var username = $('#ccn-admin-userList-inputUsername').val();
|
|
||||||
if (username == "") return;
|
|
||||||
|
|
||||||
var result = ccn_api_admin_add(username);
|
|
||||||
if (typeof(result) == 'undefined') {
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-add"));
|
|
||||||
} else {
|
|
||||||
// render
|
|
||||||
var index = ccn_admin_userListCache.push(result) - 1;
|
|
||||||
var listDOM = $('#ccn-admin-userList');
|
|
||||||
ccn_admin_userList_RenderItem(result, index, listDOM);
|
|
||||||
ccn_i18n_ApplyLanguage2Content(listDOM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_ItemEdit() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
// copy isAdmin to checkbox and clean password box
|
|
||||||
$('#ccn-userItem-inputIsAdmin-' + uuid).prop("checked", ccn_admin_userListCache[uuid][1]);
|
|
||||||
$('#ccn-userItem-inputPassword-' + uuid).val('');
|
|
||||||
|
|
||||||
// switch to edit mode
|
|
||||||
ccn_admin_userList_ChangeDisplayMode(uuid, true, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_ItemDelete() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
var result = ccn_api_admin_delete(ccn_admin_userListCache[uuid][0]);
|
|
||||||
|
|
||||||
if(!result) {
|
|
||||||
// fail
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-delete"));
|
|
||||||
} else {
|
|
||||||
// remove body
|
|
||||||
$("#ccn-userItem-" + uuid).remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_ItemUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
var newpassword = $('#ccn-userItem-inputPassword-' + uuid).val();
|
|
||||||
var isAdmin = $('#ccn-userItem-inputIsAdmin-' + uuid).prop("checked");
|
|
||||||
|
|
||||||
var result = ccn_api_admin_update(
|
|
||||||
ccn_admin_userListCache[uuid][0],
|
|
||||||
newpassword == "" ? undefined : newpassword,
|
|
||||||
isAdmin == ccn_admin_userListCache[uuid][1] ? undefined : isAdmin);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
// fail
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-update"));
|
|
||||||
} else {
|
|
||||||
// safely update data
|
|
||||||
ccn_admin_userListCache[uuid][1] = isAdmin
|
|
||||||
|
|
||||||
// switch to normal mode
|
|
||||||
ccn_admin_userList_ChangeDisplayMode(uuid, false, isAdmin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_admin_userList_ItemCancelUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
ccn_admin_userList_ChangeDisplayMode(uuid, false, undefined);
|
|
||||||
}
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
// 2 list which will store sharing and shared collection's display mode.
|
|
||||||
// key is uuid, value is bool
|
|
||||||
var ccn_calendar_owned_displayCache = [];
|
|
||||||
var ccn_calendar_shared_displayCache = [];
|
|
||||||
|
|
||||||
// modal editing object.
|
|
||||||
// undefined mean add
|
|
||||||
// not undefined mean update(a copy of calendar event)
|
|
||||||
var ccn_calendar_eventModal_editing = undefined;
|
|
||||||
var ccn_calendar_eventModal_collectionCache = [];
|
|
||||||
var ccn_calendar_calendar_listCache = [];
|
|
||||||
var ccn_calendar_calendar_displayCache = [];
|
|
||||||
var ccn_calendar_calendar_displayDateTime = 0;
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.calendar;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// process calendar it self
|
|
||||||
ccn_calendar_calendar_LoadCalendarBody();
|
|
||||||
|
|
||||||
// init datetimepicker and preset
|
|
||||||
ccn_datetimepicker_Insert();
|
|
||||||
var nowtime = new Date();
|
|
||||||
ccn_datetimepicker_Set(1, nowtime, false, ccn_datetimepicker_tabType.month);
|
|
||||||
|
|
||||||
// bind tab control switcher and set current tab
|
|
||||||
$("#tabcontrol-tab-1-1").click(function(){
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 1);
|
|
||||||
});
|
|
||||||
$("#tabcontrol-tab-1-2").click(function(){
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 2);
|
|
||||||
});
|
|
||||||
$("#tabcontrol-tab-1-3").click(function(){
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 3);
|
|
||||||
});
|
|
||||||
ccn_tabcontrol_SwitchTab(1, 1);
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
|
|
||||||
//refresh once
|
|
||||||
ccn_calendar_collection_Refresh();
|
|
||||||
ccn_calendar_calendar_Refresh();
|
|
||||||
ccn_calendar_calendar_Analyse();
|
|
||||||
ccn_calendar_calendar_Render();
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$('#ccn-calendar-collection-btnRefresh').click(ccn_calendar_collection_Refresh);
|
|
||||||
|
|
||||||
$('#ccn-calendar-calendar-btnJump')
|
|
||||||
.prop('funcs', {callback: ccn_calendar_calendar_btnRefresh})
|
|
||||||
.click(function() {
|
|
||||||
ccn_datetimepicker_Modal(
|
|
||||||
ccn_datetimepicker_tabType.month,
|
|
||||||
1,
|
|
||||||
false);
|
|
||||||
});
|
|
||||||
$('#ccn-calendar-calendar-btnToday').click(ccn_calendar_calendar_btnToday);
|
|
||||||
$('#ccn-calendar-calendar-btnAdd').click(ccn_calendar_calendar_btnAdd);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ================== calendar
|
|
||||||
|
|
||||||
function ccn_calendar_calendar_LoadCalendarBody() {
|
|
||||||
$('#ccn-calendar-calendarBody').append(ccn_template_calendarItem.render());
|
|
||||||
}
|
|
||||||
|
|
||||||
// this function only refresh cache list
|
|
||||||
function ccn_calendar_calendar_Refresh() {
|
|
||||||
var gottenDateTime = ccn_datetimepicker_Get(1, false);
|
|
||||||
var gottenYear = gottenDateTime.getFullYear();
|
|
||||||
var gottenMonth = gottenDateTime.getMonth() + 1;
|
|
||||||
$('#ccn-calendar-calendar-textMonth').text('{0} - {1}'.format(gottenYear, ccn_i18n_UniversalGetMonth(gottenMonth - 1)));
|
|
||||||
// don't need to set anything, because its default value is enough to use.
|
|
||||||
|
|
||||||
var gottenWeek = ccn_datetime_DayOfWeek(gottenYear, gottenMonth, 1);
|
|
||||||
var startTimestamp = Math.floor(gottenDateTime.getTime() / 60000) - gottenWeek * ccn_datetime_DAY1_SPAN;
|
|
||||||
var endTimestamp = startTimestamp + ccn_datetime_DAY1_SPAN * 6 * 7 - 1;
|
|
||||||
|
|
||||||
ccn_calendar_calendar_listCache = new Array();
|
|
||||||
var result = ccn_api_calendar_getFull(startTimestamp, endTimestamp);
|
|
||||||
if (typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
ccn_calendar_calendar_listCache[result[index][0]] = result[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this function take responsibility to analyse event
|
|
||||||
// call datetime function to resolve loop event
|
|
||||||
// and split event if some event cross 2+ days
|
|
||||||
function ccn_calendar_calendar_Analyse() {
|
|
||||||
// first, we need construct ccn_calendar_calendar_displayCache
|
|
||||||
ccn_calendar_calendar_displayCache = new Array();
|
|
||||||
var gottenDateTime = ccn_datetimepicker_Get(1, false);
|
|
||||||
var gottenYear = gottenDateTime.getFullYear();
|
|
||||||
var gottenMonth = gottenDateTime.getMonth() + 1;
|
|
||||||
var gottenWeek = ccn_datetime_DayOfWeek(gottenYear, gottenMonth, 1);
|
|
||||||
var startTimestamp = Math.floor(gottenDateTime.getTime() / 60000) - gottenWeek * ccn_datetime_DAY1_SPAN;
|
|
||||||
var endTimestamp = startTimestamp + ccn_datetime_DAY1_SPAN * 6 * 7 - 1;
|
|
||||||
gottenDateTime.setTime(startTimestamp * 60000);
|
|
||||||
for(var index = 0; index < 6 * 7; index++) {
|
|
||||||
ccn_calendar_calendar_displayCache.push({
|
|
||||||
month: gottenDateTime.getMonth() + 1,
|
|
||||||
day: gottenDateTime.getDate(),
|
|
||||||
dayOfWeek: gottenDateTime.getWeekday() + 1,
|
|
||||||
subcalendar: "",
|
|
||||||
isCurrentMonth: (gottenDateTime.getMonth() + 1) == gottenMonth,
|
|
||||||
events: new Array()
|
|
||||||
});
|
|
||||||
gottenDateTime.setTime(gottenDateTime.getTime() + ccn_datetime_DAY1_SPAN * 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
var mytimezone = -(new Date().getTimezoneOffset());
|
|
||||||
// then analyse each event
|
|
||||||
for(var index in ccn_calendar_calendar_listCache) {
|
|
||||||
var item = ccn_calendar_calendar_listCache[index];
|
|
||||||
var deserializedDescription = ccn_api_deserializeDescription(item[3]);
|
|
||||||
|
|
||||||
var minStartTimestamp = startTimestamp - (item[6] - item[5]);
|
|
||||||
var result = ccn_datetime_ResolveLoopRules4Event(
|
|
||||||
item[8],
|
|
||||||
item[9] < minStartTimestamp ? minStartTimestamp : item[9],
|
|
||||||
Math.min(item[10], endTimestamp),
|
|
||||||
item[5],
|
|
||||||
item[6],
|
|
||||||
item[7],
|
|
||||||
startTimestamp
|
|
||||||
);
|
|
||||||
if(typeof(result) != 'undefined') {
|
|
||||||
for(var i in result) {
|
|
||||||
var it = result[i];
|
|
||||||
// try get event belong to which cell
|
|
||||||
var eventDateTime = new Date(it[0] * 60000);
|
|
||||||
var count = Math.floor((it[0] - startTimestamp) / ccn_datetime_DAY1_SPAN);
|
|
||||||
var exitFlag = false;
|
|
||||||
// then split event
|
|
||||||
while(count < 6 * 7) {
|
|
||||||
var eventItem = {
|
|
||||||
uuid: item[0],
|
|
||||||
belongTo: item[1],
|
|
||||||
title: item[2],
|
|
||||||
description: deserializedDescription.description,
|
|
||||||
color: deserializedDescription.color,
|
|
||||||
isVisible: true,
|
|
||||||
isLocked: typeof(ccn_calendar_owned_displayCache[item[0]]) != 'undefined',
|
|
||||||
loopText: ccn_datetime_ResolveLoopRules4Text(item[8], item[5], item[7]),
|
|
||||||
timezoneWarning: mytimezone != item[7],
|
|
||||||
start: eventDateTime.toLocaleTimeString(),
|
|
||||||
end: undefined // filled in follwing code
|
|
||||||
}
|
|
||||||
eventDateTime.setHours(23, 59, 0, 0);
|
|
||||||
if (it[1] <= Math.floor(eventDateTime.getTime() / 60000)) {
|
|
||||||
exitFlag = true;
|
|
||||||
eventDateTime.setTime(it[1] * 60000);
|
|
||||||
}
|
|
||||||
eventItem.end = eventDateTime.toLocaleTimeString();
|
|
||||||
ccn_calendar_calendar_displayCache[count].events.push(eventItem);
|
|
||||||
if (exitFlag) break;
|
|
||||||
else eventDateTime.setMinutes(eventDateTime.getMinutes() + 1, 0, 0);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// just use produced ccn_calendar_calendar_displayCache
|
|
||||||
// to re-generate ui
|
|
||||||
function ccn_calendar_calendar_Render() {
|
|
||||||
// todo: add / migrate subcalendar feature here
|
|
||||||
|
|
||||||
|
|
||||||
// analyse visible data
|
|
||||||
for(var i in ccn_calendar_calendar_displayCache) {
|
|
||||||
for(var j in ccn_calendar_calendar_displayCache[i].events) {
|
|
||||||
var gottenOwnedVisible = ccn_calendar_owned_displayCache[
|
|
||||||
ccn_calendar_calendar_displayCache[i].events[j].belongTo
|
|
||||||
];
|
|
||||||
if (typeof(gottenOwnedVisible) == 'undefined') gottenOwnedVisible = false;
|
|
||||||
var gottenSharedVisible = ccn_calendar_shared_displayCache[
|
|
||||||
ccn_calendar_calendar_displayCache[i].events[j].belongTo
|
|
||||||
];
|
|
||||||
if (typeof(gottenSharedVisible) == 'undefined') gottenSharedVisible = false;
|
|
||||||
|
|
||||||
ccn_calendar_calendar_displayCache[i].events[j].isVisible = gottenOwnedVisible || gottenSharedVisible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// just render them
|
|
||||||
var listDOM = $('#ccn-calendar-scheduleList');
|
|
||||||
listDOM.empty();
|
|
||||||
listDOM.append(ccn_template_scheduleItem.render({renderdata: ccn_calendar_calendar_displayCache}));
|
|
||||||
// link click event
|
|
||||||
$('div.schedule-event-outter').click(ccn_calendar_calendar_ItemUpdate);
|
|
||||||
|
|
||||||
// all data has been alanysed, feedback to calendar body.
|
|
||||||
var counter = 0;
|
|
||||||
for(var i = 0; i < 6; i++) {
|
|
||||||
for(var j = 0; j < 7; j++) {
|
|
||||||
var item = ccn_calendar_calendar_displayCache[counter];
|
|
||||||
var lenEvents = item.events.length;
|
|
||||||
var eventsCounter = 0;
|
|
||||||
|
|
||||||
$('#ccn-calendarItem-' + i + '-' + j).attr('isCurrentMonth', item.isCurrentMonth ? 'true' : 'false');
|
|
||||||
|
|
||||||
$('#ccn-calendarItem-title-' + i + '-' + j).text(item.day);
|
|
||||||
$('#ccn-calendarItem-desc-' + i + '-' + j).text(item.subcalendar);
|
|
||||||
|
|
||||||
|
|
||||||
for(; eventsCounter < Math.min(lenEvents, 4); eventsCounter++) {
|
|
||||||
$('#ccn-calendarItem-eventBox' + (eventsCounter + 1) + '-' + i + '-' + j)
|
|
||||||
.css('background', item.events[eventsCounter].color)
|
|
||||||
.attr('enableDisplay', 'true');
|
|
||||||
}
|
|
||||||
if (lenEvents > 4) {
|
|
||||||
// more than 4 item, write number
|
|
||||||
$('#ccn-calendarItem-task-' + i + '-' + j).text(
|
|
||||||
$.i18n.prop('ccn-i18n-calendar-calendar-stripedEvents').format(lenEvents.toString())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// otherwise, wipe out number
|
|
||||||
$('#ccn-calendarItem-task-' + i + '-' + j).html(' ');
|
|
||||||
// set others div are blank
|
|
||||||
for(; eventsCounter < 4; eventsCounter++) {
|
|
||||||
$('#ccn-calendarItem-eventBox' + (eventsCounter + 1) + '-' + i + '-' + j)
|
|
||||||
.attr('enableDisplay', 'false');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ccn_i18n_ApplyLanguage2Content(listDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_calendar_btnRefresh() {
|
|
||||||
ccn_calendar_calendar_Refresh();
|
|
||||||
ccn_calendar_calendar_Analyse();
|
|
||||||
ccn_calendar_calendar_Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_calendar_btnToday() {
|
|
||||||
var nowtime = new Date();
|
|
||||||
ccn_datetimepicker_Set(1, nowtime, false, ccn_datetimepicker_tabType.month);
|
|
||||||
ccn_calendar_calendar_Refresh();
|
|
||||||
ccn_calendar_calendar_Analyse();
|
|
||||||
ccn_calendar_calendar_Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_calendar_btnAdd() {
|
|
||||||
window.location.href = '/web/eventAdd';
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_calendar_ItemUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
window.location.href = '/web/eventUpdate/' + uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= collection
|
|
||||||
|
|
||||||
function ccn_calendar_collection_Refresh() {
|
|
||||||
ccn_calendar_owned_displayCache = new Array();
|
|
||||||
ccn_calendar_shared_displayCache = new Array();
|
|
||||||
|
|
||||||
// render shared
|
|
||||||
var result = ccn_api_collection_getShared();
|
|
||||||
var listDOM = $('#ccn-calendar-sharedList');
|
|
||||||
listDOM.empty();
|
|
||||||
var renderdata = {
|
|
||||||
uuid: undefined,
|
|
||||||
name: undefined,
|
|
||||||
username: undefined
|
|
||||||
}
|
|
||||||
if (typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
var item = result[index];
|
|
||||||
renderdata.uuid = item[0];
|
|
||||||
renderdata.name = item[1];
|
|
||||||
renderdata.username = item[2];
|
|
||||||
|
|
||||||
listDOM.append(ccn_template_displaySharedItem.render(renderdata));
|
|
||||||
|
|
||||||
// change display
|
|
||||||
var uuid = renderdata.uuid;
|
|
||||||
ccn_calendar_shared_ChangeDisplayMode(uuid, true);
|
|
||||||
|
|
||||||
// push into display list
|
|
||||||
ccn_calendar_shared_displayCache[uuid] = true;
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$('#ccn-displaySharedItem-btnHide-' + uuid).click(ccn_calendar_shared_ItemSwitchDisplay);
|
|
||||||
$('#ccn-displaySharedItem-btnShow-' + uuid).click(ccn_calendar_shared_ItemSwitchDisplay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ccn_i18n_ApplyLanguage2Content(listDOM);
|
|
||||||
|
|
||||||
// render owned
|
|
||||||
result = ccn_api_collection_getFullOwn();
|
|
||||||
listDOM = $('#ccn-calendar-ownedList');
|
|
||||||
listDOM.empty();
|
|
||||||
renderdata = {
|
|
||||||
uuid: undefined,
|
|
||||||
name: undefined
|
|
||||||
}
|
|
||||||
if (typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
var item = result[index];
|
|
||||||
renderdata.uuid = item[0];
|
|
||||||
renderdata.name = item[1];
|
|
||||||
|
|
||||||
// render
|
|
||||||
listDOM.append(ccn_template_displayOwnedItem.render(renderdata));
|
|
||||||
|
|
||||||
// set mode
|
|
||||||
var uuid = renderdata.uuid;
|
|
||||||
ccn_calendar_owned_ChangeDisplayMode(uuid, true);
|
|
||||||
|
|
||||||
// push into display list
|
|
||||||
ccn_calendar_owned_displayCache[uuid] = true;
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$('#ccn-displayOwnedItem-btnHide-' + uuid).click(ccn_calendar_owned_ItemSwitchDisplay);
|
|
||||||
$('#ccn-displayOwnedItem-btnShow-' + uuid).click(ccn_calendar_owned_ItemSwitchDisplay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_owned_ItemSwitchDisplay() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
ccn_calendar_owned_displayCache[uuid] = !(ccn_calendar_owned_displayCache[uuid]);
|
|
||||||
ccn_calendar_owned_ChangeDisplayMode(uuid, ccn_calendar_owned_displayCache[uuid]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_shared_ItemSwitchDisplay() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
ccn_calendar_shared_displayCache[uuid] = !(ccn_calendar_shared_displayCache[uuid]);
|
|
||||||
ccn_calendar_shared_ChangeDisplayMode(uuid, ccn_calendar_shared_displayCache[uuid]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_shared_ChangeDisplayMode(uuid, isShow) {
|
|
||||||
if (isShow) {
|
|
||||||
$('#ccn-displaySharedItem-btnHide-' + uuid).show();
|
|
||||||
$('#ccn-displaySharedItem-btnShow-' + uuid).hide();
|
|
||||||
} else {
|
|
||||||
$('#ccn-displaySharedItem-btnHide-' + uuid).hide();
|
|
||||||
$('#ccn-displaySharedItem-btnShow-' + uuid).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_calendar_owned_ChangeDisplayMode(uuid, isShow) {
|
|
||||||
if (isShow) {
|
|
||||||
$('#ccn-displayOwnedItem-btnHide-' + uuid).show();
|
|
||||||
$('#ccn-displayOwnedItem-btnShow-' + uuid).hide();
|
|
||||||
} else {
|
|
||||||
$('#ccn-displayOwnedItem-btnHide-' + uuid).hide();
|
|
||||||
$('#ccn-displayOwnedItem-btnShow-' + uuid).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
// 3 used cache list
|
|
||||||
var ccn_collection_owned_listCache = [];
|
|
||||||
var ccn_collection_sharing_listCache = [];
|
|
||||||
|
|
||||||
// current editing sharing collection
|
|
||||||
var ccn_collection_sharing_editingOwned = undefined; // the uuid of owned collection
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.collection;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
|
|
||||||
//refresh once
|
|
||||||
ccn_collection_owned_Refresh();
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
//$('#ccn-calendar-shared-btnRefresh').click(ccn_calendar_shared_Refresh);
|
|
||||||
$('#ccn-collection-owned-btnAdd').click(ccn_collection_owned_Add);
|
|
||||||
$('#ccn-collection-owned-btnRefresh').click(ccn_collection_owned_Refresh);
|
|
||||||
$('#ccn-collection-sharing-btnAdd').click(ccn_collection_sharing_Add);
|
|
||||||
$('#ccn-collection-sharing-btnRefresh').click(ccn_collection_sharing_Refresh);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_collection_owned_Refresh() {
|
|
||||||
ccn_collection_owned_listCache = new Array();
|
|
||||||
ccn_collection_sharing_displayCache = new Array();
|
|
||||||
|
|
||||||
var result = ccn_api_collection_getFullOwn();
|
|
||||||
if(typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
ccn_collection_owned_listCache[result[index][0]] = result[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render
|
|
||||||
var listDOM = $('#ccn-collection-ownedList');
|
|
||||||
listDOM.empty();
|
|
||||||
for(var index in ccn_collection_owned_listCache) {
|
|
||||||
ccn_collection_owned_RenderItem(
|
|
||||||
ccn_collection_owned_listCache[index],
|
|
||||||
listDOM
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// also, order sharing list clean
|
|
||||||
ccn_collection_sharing_editingOwned = undefined;
|
|
||||||
ccn_collection_sharing_Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_RenderItem(item, listDOM) {
|
|
||||||
var renderdata = {
|
|
||||||
uuid: item[0],
|
|
||||||
name: item[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// render
|
|
||||||
listDOM.append(ccn_template_ownedItem.render(renderdata));
|
|
||||||
|
|
||||||
// set mode
|
|
||||||
var uuid = renderdata.uuid;
|
|
||||||
ccn_collection_owned_ChangeDisplayMode(uuid, false);
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$('#ccn-ownedItem-btnEdit-' + uuid).click(ccn_collection_owned_ItemEdit);
|
|
||||||
$('#ccn-ownedItem-btnDelete-' + uuid).click(ccn_collection_owned_ItemDelete);
|
|
||||||
$('#ccn-ownedItem-btnShare-' + uuid).click(ccn_collection_owned_ItemShare);
|
|
||||||
$('#ccn-ownedItem-btnUpdate-' + uuid).click(ccn_collection_owned_ItemUpdate);
|
|
||||||
$('#ccn-ownedItem-btnCancelUpdate-' + uuid).click(ccn_collection_owned_ItemCancelUpdate);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_ChangeDisplayMode(uuid, isEdit) {
|
|
||||||
if (isEdit) {
|
|
||||||
$('#ccn-ownedItem-btnEdit-' + uuid).hide();
|
|
||||||
$('#ccn-ownedItem-btnShare-' + uuid).hide();
|
|
||||||
$('#ccn-ownedItem-btnDelete-' + uuid).hide();
|
|
||||||
|
|
||||||
$('#ccn-ownedItem-btnUpdate-' + uuid).show();
|
|
||||||
$('#ccn-ownedItem-btnCancelUpdate-' + uuid).show();
|
|
||||||
|
|
||||||
$('#ccn-ownedItem-textName-' + uuid).hide();
|
|
||||||
$('#ccn-ownedItem-boxName-' + uuid).show();
|
|
||||||
} else {
|
|
||||||
$('#ccn-ownedItem-btnEdit-' + uuid).show();
|
|
||||||
$('#ccn-ownedItem-btnShare-' + uuid).show();
|
|
||||||
$('#ccn-ownedItem-btnDelete-' + uuid).show();
|
|
||||||
|
|
||||||
$('#ccn-ownedItem-btnUpdate-' + uuid).hide();
|
|
||||||
$('#ccn-ownedItem-btnCancelUpdate-' + uuid).hide();
|
|
||||||
|
|
||||||
$('#ccn-ownedItem-textName-' + uuid).show();
|
|
||||||
$('#ccn-ownedItem-boxName-' + uuid).hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_collection_sharing_Refresh() {
|
|
||||||
ccn_collection_sharing_listCache = new Array();
|
|
||||||
|
|
||||||
if (typeof(ccn_collection_sharing_editingOwned) != 'undefined') {
|
|
||||||
var result = ccn_api_collection_getSharing(ccn_collection_sharing_editingOwned);
|
|
||||||
if (typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
ccn_collection_sharing_listCache[index] = result[index];
|
|
||||||
// also, sharingTarget don't have uuid, use index instead
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update editing text
|
|
||||||
$('#ccn-collection-sharing-sharingEditing').text(
|
|
||||||
typeof(ccn_collection_sharing_editingOwned) == 'undefined' ?
|
|
||||||
'' :
|
|
||||||
ccn_collection_owned_listCache[ccn_collection_sharing_editingOwned][1]
|
|
||||||
);
|
|
||||||
|
|
||||||
// if editing is undefined, hide container
|
|
||||||
if (typeof(ccn_collection_sharing_editingOwned) == 'undefined')
|
|
||||||
$('#ccn-collection-sharing-container').hide();
|
|
||||||
else
|
|
||||||
$('#ccn-collection-sharing-container').show();
|
|
||||||
|
|
||||||
|
|
||||||
var listDOM = $('#ccn-collection-sharingList');
|
|
||||||
listDOM.empty();
|
|
||||||
for(var index in ccn_collection_sharing_listCache) {
|
|
||||||
ccn_collection_sharing_RenderItem(
|
|
||||||
ccn_collection_sharing_listCache[index],
|
|
||||||
index,
|
|
||||||
listDOM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_sharing_RenderItem(item, index, listDOM) {
|
|
||||||
var renderdata = {
|
|
||||||
uuid: index,
|
|
||||||
username: item
|
|
||||||
}
|
|
||||||
|
|
||||||
// render
|
|
||||||
listDOM.append(ccn_template_sharingItem.render(renderdata));
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
var uuid = index;
|
|
||||||
$("#ccn-sharingItem-btnDelete-" + uuid).click(ccn_collection_sharing_ItemDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================= input operation
|
|
||||||
|
|
||||||
function ccn_collection_owned_Add() {
|
|
||||||
var newname = $('#ccn-collection-owned-inputAdd').val();
|
|
||||||
if (newname == "") return;
|
|
||||||
|
|
||||||
var result = ccn_api_collection_addOwn(newname);
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-add"));
|
|
||||||
else {
|
|
||||||
// second get. get detail
|
|
||||||
result = ccn_api_collection_getDetailOwn(result);
|
|
||||||
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-get"));
|
|
||||||
else {
|
|
||||||
// render
|
|
||||||
ccn_collection_owned_listCache[result[0]] = result;
|
|
||||||
var listDOM = $('#ccn-collection-ownedList');
|
|
||||||
ccn_collection_owned_RenderItem(result, listDOM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_ItemEdit() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
// preset inputbox
|
|
||||||
$('#ccn-ownedItem-inputName-' + uuid).val(
|
|
||||||
ccn_collection_owned_listCache[uuid][1]
|
|
||||||
);
|
|
||||||
|
|
||||||
// switch to edit mode
|
|
||||||
ccn_collection_owned_ChangeDisplayMode(uuid, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_ItemDelete() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
var result = ccn_api_collection_deleteOwn(
|
|
||||||
uuid,
|
|
||||||
ccn_collection_owned_listCache[uuid][2]
|
|
||||||
);
|
|
||||||
if (!result) ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-delete"));
|
|
||||||
else {
|
|
||||||
$('#ccn-ownedItem-' + uuid).remove();
|
|
||||||
|
|
||||||
// also, we should notice sharing target, and try clean it
|
|
||||||
if (ccn_collection_sharing_editingOwned == uuid) {
|
|
||||||
ccn_collection_sharing_editingOwned = undefined;
|
|
||||||
ccn_collection_sharing_Refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_ItemUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
var newname = $('#ccn-ownedItem-inputName-' + uuid).val();
|
|
||||||
|
|
||||||
var result = ccn_api_collection_updateOwn(uuid, newname, ccn_collection_owned_listCache[uuid][2]);
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-update"));
|
|
||||||
else {
|
|
||||||
// update last change
|
|
||||||
ccn_collection_owned_listCache[uuid][2] = result;
|
|
||||||
ccn_collection_owned_listCache[uuid][1] = newname;
|
|
||||||
// update elements
|
|
||||||
$('#ccn-ownedItem-textName-' + uuid).text(newname);
|
|
||||||
// if editing, update sharing target
|
|
||||||
if (ccn_collection_sharing_editingOwned == uuid)
|
|
||||||
ccn_collection_sharing_Refresh();
|
|
||||||
// back to normal mode
|
|
||||||
ccn_collection_owned_ChangeDisplayMode(uuid, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_ItemCancelUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
ccn_collection_owned_ChangeDisplayMode(uuid, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_owned_ItemShare() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
ccn_collection_sharing_editingOwned = uuid;
|
|
||||||
ccn_collection_sharing_Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_collection_sharing_Add() {
|
|
||||||
var newusername = $('#ccn-collection-sharing-inputAdd').val();
|
|
||||||
if (newusername == "" || typeof(ccn_collection_sharing_editingOwned) == 'undefined') return;
|
|
||||||
|
|
||||||
var result = ccn_api_collection_addSharing(
|
|
||||||
ccn_collection_sharing_editingOwned,
|
|
||||||
newusername,
|
|
||||||
ccn_collection_owned_listCache[ccn_collection_sharing_editingOwned][2]
|
|
||||||
);
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-add"));
|
|
||||||
else {
|
|
||||||
// add new item
|
|
||||||
var index = ccn_collection_sharing_listCache.push(newusername) - 1;
|
|
||||||
var listDOM = $('#ccn-collection-sharingList');
|
|
||||||
ccn_collection_sharing_RenderItem(newusername, index, listDOM);
|
|
||||||
// update last change
|
|
||||||
ccn_collection_owned_listCache[ccn_collection_sharing_editingOwned][2] = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_collection_sharing_ItemDelete() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
var username = ccn_collection_sharing_listCache[uuid];
|
|
||||||
|
|
||||||
var result = ccn_api_collection_deleteSharing(
|
|
||||||
ccn_collection_sharing_editingOwned,
|
|
||||||
username,
|
|
||||||
ccn_collection_owned_listCache[ccn_collection_sharing_editingOwned][2]
|
|
||||||
);
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-delete"));
|
|
||||||
else {
|
|
||||||
// remove item in ui
|
|
||||||
$('#ccn-sharingItem-' + uuid).remove();
|
|
||||||
// update last change
|
|
||||||
ccn_collection_owned_listCache[ccn_collection_sharing_editingOwned][2] = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
// if it is undefined, current mode is add
|
|
||||||
// or it is the detail data gotten from api
|
|
||||||
var ccn_event_editingEvent = undefined;
|
|
||||||
var ccn_event_collectionCache = [];
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.event;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// init datetimepicker
|
|
||||||
ccn_datetimepicker_Insert();
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$('input[type=radio][name=loop-method]').click(ccn_event_RefreshRadioDiaplay);
|
|
||||||
$('input[type=radio][name=loop-end]').click(ccn_event_RefreshRadioDiaplay);
|
|
||||||
|
|
||||||
$('#ccn-event-btnSubmit').click(ccn_event_btnSubmit);
|
|
||||||
$('#ccn-event-btnCancel').click(ccn_event_btnCancel);
|
|
||||||
$('#ccn-event-btnSpot').click(ccn_event_btnSpot);
|
|
||||||
$('#ccn-event-btnFullDay').click(ccn_event_btnFullDay);
|
|
||||||
$('#ccn-event-btnStartDateTime')
|
|
||||||
.prop('funcs', {callback: function() {
|
|
||||||
ccn_event_UpdateDateTimePickerButton(1);
|
|
||||||
ccn_event_RefreshLoopMonthType();
|
|
||||||
}})
|
|
||||||
.click(ccn_event_btnDateTimePicker);
|
|
||||||
$('#ccn-event-btnEndDateTime')
|
|
||||||
.prop('funcs', {callback: function() {ccn_event_UpdateDateTimePickerButton(2);}})
|
|
||||||
.click(ccn_event_btnDateTimePicker);
|
|
||||||
$('#ccn-event-btnLoopStopDateTime')
|
|
||||||
.prop('funcs', {callback: function() {ccn_event_UpdateDateTimePickerButton(3);}})
|
|
||||||
.click(ccn_event_btnDateTimePicker);
|
|
||||||
|
|
||||||
// init form
|
|
||||||
ccn_event_Init();
|
|
||||||
|
|
||||||
// refresh once
|
|
||||||
ccn_event_RefreshRadioDiaplay();
|
|
||||||
ccn_event_RefreshLoopMonthType();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function ccn_event_Init() {
|
|
||||||
// we need init some elements first
|
|
||||||
|
|
||||||
// we need all radio and checkbox's checked is false, not undefined.
|
|
||||||
$('input[type=radio]').prop("checked", false);
|
|
||||||
$('input[type=checkbox]').prop("checked", false);
|
|
||||||
|
|
||||||
// init span picker
|
|
||||||
$('.spanpicker').attr('max', 100)
|
|
||||||
.attr('min', 1)
|
|
||||||
.attr('step', 1)
|
|
||||||
.val(1);
|
|
||||||
|
|
||||||
// in there, we need get uuid from meta
|
|
||||||
var uuid = $('meta[name=uuid]').attr('content');
|
|
||||||
if (uuid != "")
|
|
||||||
ccn_event_editingEvent = ccn_api_calendar_getDetail(uuid);
|
|
||||||
// if ccn_event_editingEvent is undefined, init following content with add mode
|
|
||||||
// otherwise, init as update mode
|
|
||||||
var isAdd = typeof(ccn_event_editingEvent) == 'undefined';
|
|
||||||
var deserializeDescription = isAdd ? undefined : ccn_api_deserializeDescription(ccn_event_editingEvent[3]);
|
|
||||||
|
|
||||||
// init title and description
|
|
||||||
$('#ccn-event-inputTitle').val(
|
|
||||||
isAdd ? '' : ccn_event_editingEvent[2]
|
|
||||||
);
|
|
||||||
$('#ccn-event-inputDescription').val(
|
|
||||||
isAdd ? '' : deserializeDescription.description
|
|
||||||
);
|
|
||||||
$('#ccn-event-inputColor').val(
|
|
||||||
isAdd ? DefaultColor : deserializeDescription.color
|
|
||||||
);
|
|
||||||
|
|
||||||
// init collection picker, first we need query data
|
|
||||||
// and render it
|
|
||||||
var collectionDOM = $('#ccn-event-inputCollection');
|
|
||||||
collectionDOM.empty();
|
|
||||||
ccn_event_collectionCache = new Array();
|
|
||||||
var result = ccn_api_collection_getFullOwn();
|
|
||||||
if (typeof(result) != 'undefined') {
|
|
||||||
var renderdata = {
|
|
||||||
val: undefined,
|
|
||||||
name: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var index in result) {
|
|
||||||
var item = result[index];
|
|
||||||
ccn_event_collectionCache.push(item[0])
|
|
||||||
renderdata.val = item[0];
|
|
||||||
renderdata.name = item[1];
|
|
||||||
collectionDOM.append(
|
|
||||||
ccn_template_optionItem.render(renderdata)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// in add mode, set as -1, otherwise try to match original data
|
|
||||||
// indexOf will return -1 if no matched item
|
|
||||||
collectionDOM.val(isAdd ? '' : ccn_event_editingEvent[1]);
|
|
||||||
|
|
||||||
// init start and end datetime
|
|
||||||
if (isAdd) {
|
|
||||||
// in add mode, init 2 datetime picker as close hours based time.
|
|
||||||
var currentDateTime = new Date();
|
|
||||||
currentDateTime.setMilliseconds(0);
|
|
||||||
currentDateTime.setSeconds(0);
|
|
||||||
currentDateTime.setMinutes(0);
|
|
||||||
ccn_datetimepicker_Set(1, currentDateTime, false);
|
|
||||||
|
|
||||||
// time span is 2 hours
|
|
||||||
currentDateTime.setHours(currentDateTime.getHours() + 2);
|
|
||||||
ccn_datetimepicker_Set(2, currentDateTime, false);
|
|
||||||
} else {
|
|
||||||
// in update mode, match it with original data
|
|
||||||
var originalDateTime = new Date((ccn_event_editingEvent[5] + ccn_event_editingEvent[7]) * 60000);
|
|
||||||
ccn_datetimepicker_Set(1, originalDateTime, true);
|
|
||||||
|
|
||||||
originalDateTime = new Date((ccn_event_editingEvent[6] + ccn_event_editingEvent[7]) * 60000);
|
|
||||||
ccn_datetimepicker_Set(2, originalDateTime, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup timezone here
|
|
||||||
// to prevent some error
|
|
||||||
// because following isAdd will change its meaning
|
|
||||||
$('#ccn-event-timezone-radioKeep').prop('checked', true); // give a default value
|
|
||||||
var nowtime = new Date();
|
|
||||||
SmarterShowHide(
|
|
||||||
(!isAdd) && (-nowtime.getTimezoneOffset()) != ccn_event_editingEvent[7],
|
|
||||||
$('#ccn-event-boxTimezone')
|
|
||||||
);
|
|
||||||
|
|
||||||
// ========================
|
|
||||||
// now we need resolve loop rules and set related data
|
|
||||||
if (!isAdd) {
|
|
||||||
data = ccn_datetime_ResolveLoopRules4UI(ccn_event_editingEvent[8]);
|
|
||||||
if (typeof(data) == 'undefined') isAdd = true; // init as add
|
|
||||||
}
|
|
||||||
|
|
||||||
// give some value with a default value
|
|
||||||
$('#ccn-event-loopMonth-radioA').prop('checked', true);
|
|
||||||
$('#ccn-event-loopWeek-check' + (nowtime.getWeekday() + 1)).prop('checked', true);
|
|
||||||
$('#ccn-event-strictMode-radioStrict').prop('checked', true);
|
|
||||||
|
|
||||||
// real process
|
|
||||||
if (isAdd) {
|
|
||||||
$('#ccn-event-radioLoopNever').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
switch(data[0][0]) {
|
|
||||||
case 0:
|
|
||||||
$('#ccn-event-radioLoopYear').prop('checked', true);
|
|
||||||
$('#ccn-event-loopYear-inputSpan').val(data[0][2]);
|
|
||||||
if (data[0][1]) $('#ccn-event-strictMode-radioStrict').prop('checked', true);
|
|
||||||
else $('#ccn-event-strictMode-radioRough').prop('checked', true);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
$('#ccn-event-radioLoopMonth').prop('checked', true);
|
|
||||||
$('#ccn-event-loopMonth-inputSpan').val(data[0][3]);
|
|
||||||
$('#ccn-event-loopMonth-radio' + data[0][2]).prop('checked', true);
|
|
||||||
if (data[0][1]) $('#ccn-event-strictMode-radioStrict').prop('checked', true);
|
|
||||||
else $('#ccn-event-strictMode-radioRough').prop('checked', true);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
$('#ccn-event-radioLoopWeek').prop('checked', true);
|
|
||||||
$('#ccn-event-loopWeek-inputSpan').val(data[0][8]);
|
|
||||||
for(var i = 1; i <= 7; i++) {
|
|
||||||
$('#ccn-event-loopWeek-check' + i).prop('checked', data[0][i]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
$('#ccn-event-radioLoopDay').prop('checked', true);
|
|
||||||
$('#ccn-event-loopDay-inputSpan').val(data[0][1]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// give some item a default value
|
|
||||||
ccn_datetimepicker_Set(3, nowtime, false);
|
|
||||||
|
|
||||||
if (isAdd) {
|
|
||||||
$('#ccn-event-loopStop-radioForever').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
switch(data[1][0]) {
|
|
||||||
case 0:
|
|
||||||
$('#ccn-event-loopStop-radioForever').prop('checked', true);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
$('#ccn-event-loopStop-radioDateTime').prop('checked', true);
|
|
||||||
var stopDatetime = new Date((data[1][1] + ccn_event_editingEvent[7]) * 60000);
|
|
||||||
ccn_datetimepicker_Set(3, stopDatetime, true);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
$('#ccn-event-loopStop-radioTimes').prop('checked', true);
|
|
||||||
$('#ccn-event-loopStop-inputTimes').val(data[1][1]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh some ui element according to form options
|
|
||||||
function ccn_event_RefreshRadioDiaplay() {
|
|
||||||
// loop method
|
|
||||||
// note: no loop control loop stop's display
|
|
||||||
// note: year and month loop also control strict mode display
|
|
||||||
SmarterShowHide(!$('#ccn-event-radioLoopNever').prop('checked'), $('#ccn-event-boxLoopStop'));
|
|
||||||
|
|
||||||
SmarterShowHide($('#ccn-event-radioLoopDay').prop('checked'), $('#ccn-event-boxLoopDay'));
|
|
||||||
SmarterShowHide($('#ccn-event-radioLoopWeek').prop('checked'), $('#ccn-event-boxLoopWeek'));
|
|
||||||
SmarterShowHide($('#ccn-event-radioLoopMonth').prop('checked'), $('#ccn-event-boxLoopMonth'));
|
|
||||||
SmarterShowHide($('#ccn-event-radioLoopYear').prop('checked'), $('#ccn-event-boxLoopYear'));
|
|
||||||
|
|
||||||
SmarterShowHide(
|
|
||||||
$('#ccn-event-radioLoopMonth').prop('checked') || $('#ccn-event-radioLoopYear').prop('checked'),
|
|
||||||
$('#ccn-event-boxStrictMode')
|
|
||||||
);
|
|
||||||
|
|
||||||
// loop stop
|
|
||||||
SmarterShowHide($('#ccn-event-loopStop-radioForever').prop('checked'), undefined);
|
|
||||||
SmarterShowHide($('#ccn-event-loopStop-radioDateTime').prop('checked'), $('#ccn-event-boxLoopStopDateTime'));
|
|
||||||
SmarterShowHide($('#ccn-event-loopStop-radioTimes').prop('checked'), $('#ccn-event-boxLoopStopTimes'));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_RefreshLoopMonthType() {
|
|
||||||
var picker = ccn_datetimepicker_Get(1, false);
|
|
||||||
var data = ccn_datetime_GetDayInMonth(picker.getFullYear(), picker.getMonth() + 1, picker.getDate());
|
|
||||||
|
|
||||||
$('#ccn-event-loopMonth-textA').text($.i18n.prop('ccn-i18n-event-loopWeek-optionA').format(data[0]));
|
|
||||||
$('#ccn-event-loopMonth-textB').text($.i18n.prop('ccn-i18n-event-loopWeek-optionB').format(data[1]));
|
|
||||||
$('#ccn-event-loopMonth-textC').text($.i18n.prop('ccn-i18n-event-loopWeek-optionC').format(data[2], data[3] + 1));
|
|
||||||
$('#ccn-event-loopMonth-textD').text($.i18n.prop('ccn-i18n-event-loopWeek-optionD').format(data[4], data[5] + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_UpdateDateTimePickerButton(index) {
|
|
||||||
switch(index) {
|
|
||||||
case 1:
|
|
||||||
$('#ccn-event-btnStartDateTime-text').text(
|
|
||||||
ccn_datetimepicker_Get(1, false).toLocaleString()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
$('#ccn-event-btnEndDateTime-text').text(
|
|
||||||
ccn_datetimepicker_Get(2, false).toLocaleString()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
$('#ccn-event-btnLoopStopDateTime-text').text(
|
|
||||||
ccn_datetimepicker_Get(3, false).toLocaleDateString()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return undefined to indicate an error
|
|
||||||
// or
|
|
||||||
// [belongTo, title, description, eventDateTimeStart, eventDateTimeEnd, timezoneOffset, loopRules]
|
|
||||||
function ccn_event_GetForm() {
|
|
||||||
// basic
|
|
||||||
var title = $('#ccn-event-inputTitle').val();
|
|
||||||
if (title == '') return undefined;
|
|
||||||
var description = $('#ccn-event-inputDescription').val();
|
|
||||||
if (description == '') return undefined;
|
|
||||||
var color = $('#ccn-event-inputColor').val();
|
|
||||||
if (color == '') return undefined;
|
|
||||||
var belongTo = $('#ccn-event-inputCollection').val();
|
|
||||||
if (belongTo == null) return undefined; // if no selected item, val return null, not undefined
|
|
||||||
|
|
||||||
var isAdd = typeof(ccn_event_editingEvent) == 'undefined';
|
|
||||||
var keepTimezone = $('#ccn-event-timezone-radioKeep').prop('checked');
|
|
||||||
var isStrict = $('#ccn-event-strictMode-radioStrict').prop('checked');
|
|
||||||
|
|
||||||
// time
|
|
||||||
var eventDateTimeStart = undefined;
|
|
||||||
var eventDateTimeEnd = undefined;
|
|
||||||
var timezoneOffset = undefined;
|
|
||||||
if ((!isAdd) && (!keepTimezone)) {
|
|
||||||
// get datetime as utc, then minus original timezone to get unix timestamp
|
|
||||||
timezoneOffset = ccn_event_editingEvent[7]; // keep timezone
|
|
||||||
eventDateTimeStart = Math.floor(ccn_datetimepicker_Get(1, true).getTime() / 60000) - timezoneOffset;
|
|
||||||
eventDateTimeEnd = Math.floor(ccn_datetimepicker_Get(2, true).getTime() / 60000) - timezoneOffset;
|
|
||||||
} else {
|
|
||||||
// use my timezone, resolve presented data as my local time
|
|
||||||
var cache = ccn_datetimepicker_Get(1, false);
|
|
||||||
timezoneOffset = -cache.getTimezoneOffset();
|
|
||||||
eventDateTimeStart = Math.floor(cache.getTime() / 60000);
|
|
||||||
eventDateTimeEnd = Math.floor(ccn_datetimepicker_Get(2, false).getTime() / 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// loopRules
|
|
||||||
var loopRules = undefined;
|
|
||||||
if ($('#ccn-event-radioLoopNever').prop('checked')) {
|
|
||||||
loopRules = "";
|
|
||||||
} else if ($('#ccn-event-radioLoopDay').prop('checked')) {
|
|
||||||
loopRules = "D{0}".format($('#ccn-event-loopDay-inputSpan').val());
|
|
||||||
} else if ($('#ccn-event-radioLoopWeek').prop('checked')) {
|
|
||||||
var cache = ""
|
|
||||||
for(var i = 1; i < 8; i++)
|
|
||||||
cache += $('#ccn-event-loopWeek-check' + i).prop('checked') ? 'T' : 'F';
|
|
||||||
loopRules = 'W{0}{1}'.format(
|
|
||||||
cache,
|
|
||||||
$('#ccn-event-loopWeek-inputSpan').val()
|
|
||||||
);
|
|
||||||
} else if ($('#ccn-event-radioLoopMonth').prop('checked')) {
|
|
||||||
var cache = undefined;
|
|
||||||
if ($('#ccn-event-loopMonth-radioA').prop('checked')) cache='A';
|
|
||||||
else if ($('#ccn-event-loopMonth-radioB').prop('checked')) cache='B';
|
|
||||||
else if ($('#ccn-event-loopMonth-radioC').prop('checked')) cache='C';
|
|
||||||
else if ($('#ccn-event-loopMonth-radioD').prop('checked')) cache='D';
|
|
||||||
else return undefined;
|
|
||||||
|
|
||||||
loopRules = "M{0}{1}{2}".format(
|
|
||||||
isStrict ? "S" : "R",
|
|
||||||
cache,
|
|
||||||
$('#ccn-event-loopMonth-inputSpan').val()
|
|
||||||
);
|
|
||||||
} else if ($('#ccn-event-radioLoopYear').prop('checked')) {
|
|
||||||
loopRules = "Y{0}{1}".format(
|
|
||||||
isStrict ? "S" : "R",
|
|
||||||
$('#ccn-event-loopYear-inputSpan').val()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need to process stop if this is not a loop event
|
|
||||||
if (loopRules != "") {
|
|
||||||
loopRules += '-';
|
|
||||||
if ($('#ccn-event-loopStop-radioForever').prop('checked')) {
|
|
||||||
loopRules += 'F';
|
|
||||||
} else if ($('#ccn-event-loopStop-radioDateTime').prop('checked')) {
|
|
||||||
var timestamp = undefined;
|
|
||||||
if ((!isAdd) && (!keepTimezone)) {
|
|
||||||
// keep timezone
|
|
||||||
var cache = ccn_datetimepicker_Get(3, true);
|
|
||||||
cache.setUTCHours(23);
|
|
||||||
cache.setUTCMinutes(59);
|
|
||||||
timestamp = Math.floor(cache.getTime() / 60000) - timezoneOffset;
|
|
||||||
} else {
|
|
||||||
// use my timezone
|
|
||||||
timestamp = Math.floor(ccn_datetimepicker_Get(3, false).getTime() / 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
loopRules += 'D{0}'.format(timestamp);
|
|
||||||
} else if ($('#ccn-event-loopStop-radioTimes').prop('checked')) {
|
|
||||||
loopRules += 'T{0}'.format($('#ccn-event-loopStop-inputTimes').val());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
belongTo,
|
|
||||||
title,
|
|
||||||
ccn_api_serializeDescription(
|
|
||||||
description,
|
|
||||||
color
|
|
||||||
),
|
|
||||||
eventDateTimeStart,
|
|
||||||
eventDateTimeEnd,
|
|
||||||
timezoneOffset,
|
|
||||||
loopRules
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_btnSpot() {
|
|
||||||
var datetime = ccn_datetimepicker_Get(1, false);
|
|
||||||
datetime.setMinutes(datetime.getMinutes() + 1);
|
|
||||||
ccn_datetimepicker_Set(2, datetime, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_btnFullDay() {
|
|
||||||
var datetime = ccn_datetimepicker_Get(1, false);
|
|
||||||
datetime.setMinutes(0);
|
|
||||||
datetime.setHours(0);
|
|
||||||
ccn_datetimepicker_Set(1, datetime, false);
|
|
||||||
datetime.setMinutes(59);
|
|
||||||
datetime.setHours(23);
|
|
||||||
ccn_datetimepicker_Set(2, datetime, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_btnDateTimePicker() {
|
|
||||||
switch(parseInt($(this).attr('datetimepicker'))) {
|
|
||||||
case 1:
|
|
||||||
ccn_datetimepicker_Modal(ccn_datetimepicker_tabType.minute, 1, false);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
ccn_datetimepicker_Modal(ccn_datetimepicker_tabType.minute, 2, false);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
ccn_datetimepicker_Modal(ccn_datetimepicker_tabType.day, 3, false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_btnCancel() {
|
|
||||||
window.location.href = '/web/calendar';
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_event_btnSubmit() {
|
|
||||||
var submitData = ccn_event_GetForm();
|
|
||||||
if (typeof(submitData) == 'undefined') {
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-form"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isAdd = typeof(ccn_event_editingEvent) == 'undefined';
|
|
||||||
if (isAdd) {
|
|
||||||
var result = ccn_api_calendar_add(
|
|
||||||
submitData[0],
|
|
||||||
submitData[1],
|
|
||||||
submitData[2],
|
|
||||||
submitData[3],
|
|
||||||
submitData[4],
|
|
||||||
submitData[6],
|
|
||||||
submitData[5]
|
|
||||||
);
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-add"));
|
|
||||||
else window.location.href = '/web/calendar';
|
|
||||||
} else {
|
|
||||||
var result = ccn_api_calendar_update(
|
|
||||||
ccn_event_editingEvent[0],
|
|
||||||
ccn_event_editingEvent[1] == submitData[0] ? undefined : submitData[0],
|
|
||||||
ccn_event_editingEvent[2] == submitData[1] ? undefined : submitData[1],
|
|
||||||
ccn_event_editingEvent[3] == submitData[2] ? undefined : submitData[2],
|
|
||||||
ccn_event_editingEvent[5] == submitData[3] ? undefined : submitData[3],
|
|
||||||
ccn_event_editingEvent[6] == submitData[4] ? undefined : submitData[4],
|
|
||||||
ccn_event_editingEvent[8] == submitData[6] ? undefined : submitData[6],
|
|
||||||
ccn_event_editingEvent[7] == submitData[5] ? undefined : submitData[5],
|
|
||||||
ccn_event_editingEvent[4]
|
|
||||||
);
|
|
||||||
if (typeof(result) == 'undefined') ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-update"));
|
|
||||||
else window.location.href = '/web/calendar';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.home;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.login;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
|
|
||||||
// bind login event
|
|
||||||
$("#ccn-login-form-login").click(ccn_login_startLogin);
|
|
||||||
});
|
|
||||||
|
|
||||||
function ccn_login_startLogin() {
|
|
||||||
// disable all ui first
|
|
||||||
$("#ccn-login-form-login").attr("disabled",true);
|
|
||||||
$("#ccn-login-form-username").attr("disabled",true);
|
|
||||||
$("#ccn-login-form-password").attr("disabled",true);
|
|
||||||
|
|
||||||
// get form data
|
|
||||||
username = $("#ccn-login-form-username").val();
|
|
||||||
password = $("#ccn-login-form-password").val();
|
|
||||||
|
|
||||||
/*
|
|
||||||
// try get salt
|
|
||||||
if (ccn_api_common_salt(username)) {
|
|
||||||
// continue login
|
|
||||||
if (ccn_api_common_login(username, password)) {
|
|
||||||
// ok, logged
|
|
||||||
// jump into home page again
|
|
||||||
window.location.href = '/web/home';
|
|
||||||
|
|
||||||
} else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
|
|
||||||
} else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
|
|
||||||
*/
|
|
||||||
if (ccn_api_common_webLogin(username, password)) {
|
|
||||||
// ok, logged
|
|
||||||
// jump into home page again
|
|
||||||
window.location.href = '/web/home';
|
|
||||||
return;
|
|
||||||
|
|
||||||
} else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
|
|
||||||
|
|
||||||
// retore ui
|
|
||||||
$("#ccn-login-form-login").removeAttr("disabled");
|
|
||||||
$("#ccn-login-form-username").removeAttr("disabled");
|
|
||||||
$("#ccn-login-form-password").removeAttr("disabled");
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
var ccn_todo_todoListCache = [];
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ccn_pages_currentPage = ccn_pages_enumPages.todo;
|
|
||||||
|
|
||||||
// template process
|
|
||||||
ccn_template_Load();
|
|
||||||
|
|
||||||
// nav process
|
|
||||||
ccn_headerNav_Insert();
|
|
||||||
ccn_headerNav_BindEvents();
|
|
||||||
ccn_headerNav_LoggedRefresh();
|
|
||||||
|
|
||||||
// messagebox process
|
|
||||||
ccn_messagebox_Insert();
|
|
||||||
ccn_messagebox_BindEvent();
|
|
||||||
|
|
||||||
// apply i18n
|
|
||||||
ccn_i18n_LoadLanguage();
|
|
||||||
ccn_i18n_ApplyLanguage();
|
|
||||||
|
|
||||||
// refresh once
|
|
||||||
ccn_todo_Refresh();
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$("#ccn-todo-btnAdd").click(ccn_todo_Add);
|
|
||||||
$("#ccn-todo-btnRefresh").click(ccn_todo_Refresh);
|
|
||||||
});
|
|
||||||
|
|
||||||
function ccn_todo_RefreshCacheList() {
|
|
||||||
// clean list cache first
|
|
||||||
ccn_todo_todoListCache = new Array();
|
|
||||||
|
|
||||||
var result = ccn_api_todo_getFull();
|
|
||||||
if(typeof(result) != 'undefined') {
|
|
||||||
for(var index in result) {
|
|
||||||
ccn_todo_todoListCache[result[index][0]] = result[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_RenderCacheList() {
|
|
||||||
// clean list first
|
|
||||||
$("#ccn-todo-todoList").empty();
|
|
||||||
|
|
||||||
var renderdata = {
|
|
||||||
uuid: undefined,
|
|
||||||
data: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
var listDOM = $("#ccn-todo-todoList");
|
|
||||||
for(var index in ccn_todo_todoListCache) {
|
|
||||||
// update render data
|
|
||||||
var item = ccn_todo_todoListCache[index];
|
|
||||||
renderdata.uuid = item[0];
|
|
||||||
renderdata.data = LineBreaker2Br(item[2]);
|
|
||||||
|
|
||||||
// render
|
|
||||||
listDOM.append(ccn_template_todoItem.render(renderdata));
|
|
||||||
|
|
||||||
// set mode
|
|
||||||
var uuid = renderdata.uuid;
|
|
||||||
ccn_todo_ChangeDisplayMode(uuid, false);
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$("#ccn-todoItem-btnEdit-" + uuid).click(ccn_todo_ItemEdit);
|
|
||||||
$("#ccn-todoItem-btnDelete-" + uuid).click(ccn_todo_ItemDelete);
|
|
||||||
$("#ccn-todoItem-btnUpdate-" + uuid).click(ccn_todo_ItemUpdate);
|
|
||||||
$("#ccn-todoItem-btnCancelUpdate-" + uuid).click(ccn_todo_ItemCancelUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_ChangeDisplayMode(uuid, isEdit) {
|
|
||||||
if(isEdit) {
|
|
||||||
// 4 buttons
|
|
||||||
$("#ccn-todoItem-btnEdit-" + uuid).hide();
|
|
||||||
$("#ccn-todoItem-btnDelete-" + uuid).hide();
|
|
||||||
$("#ccn-todoItem-btnUpdate-" + uuid).show();
|
|
||||||
$("#ccn-todoItem-btnCancelUpdate-" + uuid).show();
|
|
||||||
|
|
||||||
// 2 elements
|
|
||||||
$("#ccn-todoItem-p-" + uuid).hide();
|
|
||||||
$("#ccn-todoItem-textarea-" + uuid).show();
|
|
||||||
} else {
|
|
||||||
$("#ccn-todoItem-btnEdit-" + uuid).show();
|
|
||||||
$("#ccn-todoItem-btnDelete-" + uuid).show();
|
|
||||||
$("#ccn-todoItem-btnUpdate-" + uuid).hide();
|
|
||||||
$("#ccn-todoItem-btnCancelUpdate-" + uuid).hide();
|
|
||||||
|
|
||||||
$("#ccn-todoItem-p-" + uuid).show();
|
|
||||||
$("#ccn-todoItem-textarea-" + uuid).hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_Refresh() {
|
|
||||||
// refresh and render once
|
|
||||||
ccn_todo_RefreshCacheList();
|
|
||||||
ccn_todo_RenderCacheList();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_Add() {
|
|
||||||
var result = ccn_api_todo_add();
|
|
||||||
if (typeof(result) == 'undefined') {
|
|
||||||
// fail
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-add"));
|
|
||||||
} else {
|
|
||||||
// add into cache
|
|
||||||
ccn_todo_todoListCache[result[0]] = result;
|
|
||||||
|
|
||||||
// render
|
|
||||||
var listDOM = $("#ccn-todo-todoList");
|
|
||||||
listDOM.append(ccn_template_todoItem.render({
|
|
||||||
uuid: result[0],
|
|
||||||
data: LineBreaker2Br(result[2])
|
|
||||||
}));
|
|
||||||
|
|
||||||
// set mode
|
|
||||||
var uuid = result[0];
|
|
||||||
ccn_todo_ChangeDisplayMode(uuid, false);
|
|
||||||
|
|
||||||
// bind event
|
|
||||||
$("#ccn-todoItem-btnEdit-" + uuid).click(ccn_todo_ItemEdit);
|
|
||||||
$("#ccn-todoItem-btnDelete-" + uuid).click(ccn_todo_ItemDelete);
|
|
||||||
$("#ccn-todoItem-btnUpdate-" + uuid).click(ccn_todo_ItemUpdate);
|
|
||||||
$("#ccn-todoItem-btnCancelUpdate-" + uuid).click(ccn_todo_ItemCancelUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_ItemEdit() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
// copy current data to textarea
|
|
||||||
$("#ccn-todoItem-textarea-" + uuid).val(
|
|
||||||
ccn_todo_todoListCache[uuid][2]
|
|
||||||
);
|
|
||||||
|
|
||||||
// switch to edit mode
|
|
||||||
ccn_todo_ChangeDisplayMode(uuid, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_ItemDelete() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
var result = ccn_api_todo_delete(
|
|
||||||
uuid,
|
|
||||||
ccn_todo_todoListCache[uuid][3]
|
|
||||||
);
|
|
||||||
|
|
||||||
if(!result) {
|
|
||||||
// fail
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-delete"));
|
|
||||||
} else {
|
|
||||||
// remove body
|
|
||||||
$("#ccn-todoItem-" + uuid).remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_ItemUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
|
|
||||||
var newData = $("#ccn-todoItem-textarea-" + uuid).val();
|
|
||||||
var result = ccn_api_todo_update(
|
|
||||||
uuid,
|
|
||||||
newData,
|
|
||||||
ccn_todo_todoListCache[uuid][3]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof(result) == 'undefined') {
|
|
||||||
// fail
|
|
||||||
ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-update"));
|
|
||||||
} else {
|
|
||||||
// safely update data & lastChanged and control
|
|
||||||
ccn_todo_todoListCache[uuid][2] = newData;
|
|
||||||
ccn_todo_todoListCache[uuid][3] = result;
|
|
||||||
$("#ccn-todoItem-p-" + uuid).html(LineBreaker2Br(newData));
|
|
||||||
|
|
||||||
// switch to normal mode
|
|
||||||
ccn_todo_ChangeDisplayMode(uuid, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_todo_ItemCancelUpdate() {
|
|
||||||
var uuid = $(this).attr("uuid");
|
|
||||||
// clean data
|
|
||||||
$("#ccn-todoItem-textarea-" + uuid).val("");
|
|
||||||
// switch to normal mode
|
|
||||||
ccn_todo_ChangeDisplayMode(uuid, false);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// all args are based on 1
|
|
||||||
function ccn_tabcontrol_SwitchTab(tabcontrolGroup, targetTabIndex) {
|
|
||||||
// close all panel and tab
|
|
||||||
$(".tabcontrol-tab-" + tabcontrolGroup).removeClass("is-active");
|
|
||||||
$(".tabcontrol-panel-" + tabcontrolGroup).hide();
|
|
||||||
|
|
||||||
// show specific
|
|
||||||
$("#tabcontrol-tab-" + tabcontrolGroup + "-" + targetTabIndex).addClass("is-active");
|
|
||||||
$("#tabcontrol-panel-" + tabcontrolGroup + "-" + targetTabIndex).show();
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
var ccn_template_headerNav = undefined;
|
|
||||||
var ccn_template_messagebox = undefined;
|
|
||||||
var ccn_template_datetimepicker = undefined;
|
|
||||||
var ccn_template_calendarItem = undefined;
|
|
||||||
var ccn_template_scheduleItem = undefined;
|
|
||||||
var ccn_template_ownedItem = undefined;
|
|
||||||
var ccn_template_sharingItem = undefined;
|
|
||||||
var ccn_template_displayOwnedItem = undefined;
|
|
||||||
var ccn_template_displaySharedItem = undefined;
|
|
||||||
var ccn_template_userItem = undefined;
|
|
||||||
var ccn_template_todoItem = undefined;
|
|
||||||
var ccn_template_optionItem = undefined;
|
|
||||||
var ccn_template_tokenItem = undefined;
|
|
||||||
|
|
||||||
function ccn_template_Load() {
|
|
||||||
ccn_template_headerNav = ccn_template_TemplateLoader('headerNav');
|
|
||||||
ccn_template_messagebox = ccn_template_TemplateLoader('messagebox');
|
|
||||||
ccn_template_datetimepicker = ccn_template_TemplateLoader('datetimepicker');
|
|
||||||
|
|
||||||
ccn_template_calendarItem = ccn_template_TemplateLoader('calendarItem');
|
|
||||||
ccn_template_scheduleItem = ccn_template_TemplateLoader('scheduleItem');
|
|
||||||
ccn_template_displayOwnedItem = ccn_template_TemplateLoader('displayOwnedItem');
|
|
||||||
ccn_template_displaySharedItem = ccn_template_TemplateLoader('displaySharedItem');
|
|
||||||
|
|
||||||
ccn_template_todoItem = ccn_template_TemplateLoader('todoItem');
|
|
||||||
ccn_template_userItem = ccn_template_TemplateLoader('userItem');
|
|
||||||
ccn_template_tokenItem = ccn_template_TemplateLoader('tokenItem');
|
|
||||||
|
|
||||||
ccn_template_ownedItem = ccn_template_TemplateLoader('ownedItem');
|
|
||||||
ccn_template_sharingItem = ccn_template_TemplateLoader('sharingItem');
|
|
||||||
ccn_template_optionItem = ccn_template_TemplateLoader('optionItem');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function ccn_template_TemplateLoader(templateName) {
|
|
||||||
var elements = $("#jsrender-tmpl-" + templateName);
|
|
||||||
if (elements.length == 0) return undefined;
|
|
||||||
var cache = undefined;
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: elements.attr('src'),
|
|
||||||
type: "GET",
|
|
||||||
async: false,
|
|
||||||
success: function (data) {
|
|
||||||
cache = $.templates(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
function ComputPasswordWithSalt(password, salt) {
|
|
||||||
return ComputeSHA256(ComputeSHA256(password) + salt.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComputeSHA256(strl) {
|
|
||||||
var tempstr = new TextEncoder().encode(strl);
|
|
||||||
var hashedStrl = undefined
|
|
||||||
var shitpromise = crypto.subtle.digest('SHA-256', tempstr);
|
|
||||||
Promise.all(shitpromise).then(function(result) {
|
|
||||||
hashedStrl = result;
|
|
||||||
});
|
|
||||||
var hashArray = Array.from(new Uint8Array(hashedStrl));
|
|
||||||
var hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('');
|
|
||||||
return hashHex.toLowerCase();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
var DefaultColor = '#536dfe';
|
|
||||||
|
|
||||||
function IsResponseOK(data) {
|
|
||||||
if (typeof (data) == 'undefined') {
|
|
||||||
console.log("Fail to execute an api!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!data['success']) {
|
|
||||||
console.log("Fail to execute an api! Reason:");
|
|
||||||
console.log(data['error']);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LineBreaker2Br(strl) {
|
|
||||||
return $('<div>').text(strl).html().replace(/\n/g, '<br />');
|
|
||||||
}
|
|
||||||
|
|
||||||
function IsUndefinedOrEmpty(data) {
|
|
||||||
return (typeof (data) == 'undefined' || data == "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function SmarterShowHide(boolean, element) {
|
|
||||||
if (typeof (element) == 'undefined') return;
|
|
||||||
if (boolean) element.show();
|
|
||||||
else element.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function GCD(a, b) {
|
|
||||||
if (b == 0) return a;
|
|
||||||
return GCD(b, a % b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LCM(a, b) {
|
|
||||||
return a / GCD(a, b) * b;
|
|
||||||
}
|
|
||||||
|
|
||||||
String.prototype.format = function() {
|
|
||||||
var e = arguments;
|
|
||||||
return !!this && this.replace(
|
|
||||||
/\{(\d+)\}/g,
|
|
||||||
function (t, n) {
|
|
||||||
return e[n].toString() ? e[n].toString() : t;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Date.prototype.getWeekday = function() {
|
|
||||||
var temp = this.getDay();
|
|
||||||
if (temp == 0) return 6;
|
|
||||||
else return temp - 1;
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{{for start=0 end=6 step=1 itemVar="~row"}}
|
|
||||||
<div>
|
|
||||||
{{for start=0 end=7 step=1 itemVar="~column"}}
|
|
||||||
<div id="ccn-calendarItem-{{:~row}}-{{:~column}}">
|
|
||||||
<p><b id="ccn-calendarItem-title-{{:~row}}-{{:~column}}"> </b>
|
|
||||||
<span id="ccn-calendarItem-desc-{{:~row}}-{{:~column}}"></span>
|
|
||||||
</p>
|
|
||||||
<div id="ccn-calendarItem-eventBox1-{{:~row}}-{{:~column}}" class="calendarItem-eventBox" enableDisplay="false"></div>
|
|
||||||
<div id="ccn-calendarItem-eventBox2-{{:~row}}-{{:~column}}" class="calendarItem-eventBox" enableDisplay="false"></div>
|
|
||||||
<div id="ccn-calendarItem-eventBox3-{{:~row}}-{{:~column}}" class="calendarItem-eventBox" enableDisplay="false"></div>
|
|
||||||
<div id="ccn-calendarItem-eventBox4-{{:~row}}-{{:~column}}" class="calendarItem-eventBox" enableDisplay="false"></div>
|
|
||||||
<p id="ccn-calendarItem-task-{{:~row}}-{{:~column}}"> </p>
|
|
||||||
</div>
|
|
||||||
{{/for}}
|
|
||||||
</div>
|
|
||||||
{{/for}}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<div id="ccn-datetimepicker-modal" class="modal" style="float: left; position: fixed; top: 0; bottom: 0; left: 0; right: 0;">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card" style="height: 70%;">
|
|
||||||
<header class="modal-card-head pickerHeader">
|
|
||||||
<div type="year"><small i18n-name="ccn-i18n-universal-text-year"></small><span id="ccn-datetimepicker-datetime-year"> </span></div>
|
|
||||||
<div type="month"><small i18n-name="ccn-i18n-universal-text-month"></small><span id="ccn-datetimepicker-datetime-month"> </span></div>
|
|
||||||
<div type="day"><small i18n-name="ccn-i18n-universal-text-day"></small><span id="ccn-datetimepicker-datetime-day"> </span></div>
|
|
||||||
<div type="hour"><small i18n-name="ccn-i18n-universal-text-hour"></small><span id="ccn-datetimepicker-datetime-hour"> </span></div>
|
|
||||||
<div type="minute"><small i18n-name="ccn-i18n-universal-text-minute"></small><span id="ccn-datetimepicker-datetime-minute"> </span></div>
|
|
||||||
</header>
|
|
||||||
<div class="modal-card-body pickerContainer">
|
|
||||||
<div id="ccn-datetimepicker-panelYear">
|
|
||||||
<nav class="level is-mobile">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-datetimepiacker-panelYear-prevBtn" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-datetimepiacker-panelYear-title" class="level-item"></div>
|
|
||||||
<div class="level-right">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-datetimepiacker-panelYear-nextBtn" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="ccn-datetimepiacker-panelYear-table" class="perfectTable">
|
|
||||||
<div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-datetimepicker-panelMonth">
|
|
||||||
<nav class="level is-mobile">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-datetimepiacker-panelMonth-prevBtn" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-datetimepiacker-panelMonth-title" class="level-item"></div>
|
|
||||||
<div class="level-right">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-datetimepiacker-panelMonth-nextBtn" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="ccn-datetimepiacker-panelMonth-table" class="perfectTable">
|
|
||||||
<div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-1"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-2"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-3"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-4"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-5"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-6"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-7"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-8"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-9"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-10"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-11"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-month-12"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-datetimepicker-panelDay">
|
|
||||||
<nav class="level is-mobile">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-datetimepiacker-panelDay-prevBtn" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-datetimepiacker-panelDay-title" class="level-item"></div>
|
|
||||||
<div class="level-right">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-datetimepiacker-panelDay-nextBtn" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="ccn-datetimepiacker-panelDay-table" class="perfectTable">
|
|
||||||
<div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-1"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-2"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-3"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-4"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-5"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-6"></div>
|
|
||||||
<div i18n-name="ccn-i18n-universal-week-7"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div><div></div><div></div><div></div><div></div><div></div><div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div><div></div><div></div><div></div><div></div><div></div><div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div><div></div><div></div><div></div><div></div><div></div><div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div><div></div><div></div><div></div><div></div><div></div><div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div><div></div><div></div><div></div><div></div><div></div><div></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div></div><div></div><div></div><div></div><div></div><div></div><div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg id="ccn-datetimepicker-panelHour" xmlns="http://www.w3.org/2000/svg" version="1.1" preserveAspectRatio="xMidYMid" viewBox="0 0 200 200">
|
|
||||||
<circle cx="100.000000" cy="100.000000" r="100.000000" type="background"></circle>
|
|
||||||
<line x1="100" y1="100" x2="100.000000" y2="20.000000"></line>
|
|
||||||
<circle cx="100.000000" cy="20.000000" r="1em" type="symbol"></circle>
|
|
||||||
|
|
||||||
<text x="100.000000" y="20.000000">0</text>
|
|
||||||
<text x="140.000000" y="30.717968">1</text>
|
|
||||||
<text x="169.282032" y="60.000000">2</text>
|
|
||||||
<text x="180.000000" y="100.000000">3</text>
|
|
||||||
<text x="169.282032" y="140.000000">4</text>
|
|
||||||
<text x="140.000000" y="169.282032">5</text>
|
|
||||||
<text x="100.000000" y="180.000000">6</text>
|
|
||||||
<text x="60.000000" y="169.282032">7</text>
|
|
||||||
<text x="30.717968" y="140.000000">8</text>
|
|
||||||
<text x="20.000000" y="100.000000">9</text>
|
|
||||||
<text x="30.717968" y="60.000000">10</text>
|
|
||||||
<text x="60.000000" y="30.717968">11</text>
|
|
||||||
<text x="100.000000" y="40.000000">12</text>
|
|
||||||
<text x="130.000000" y="48.038476">13</text>
|
|
||||||
<text x="151.961524" y="70.000000">14</text>
|
|
||||||
<text x="160.000000" y="100.000000">15</text>
|
|
||||||
<text x="151.961524" y="130.000000">16</text>
|
|
||||||
<text x="130.000000" y="151.961524">17</text>
|
|
||||||
<text x="100.000000" y="160.000000">18</text>
|
|
||||||
<text x="70.000000" y="151.961524">19</text>
|
|
||||||
<text x="48.038476" y="130.000000">20</text>
|
|
||||||
<text x="40.000000" y="100.000000">21</text>
|
|
||||||
<text x="48.038476" y="70.000000">22</text>
|
|
||||||
<text x="70.000000" y="48.038476">23</text>
|
|
||||||
</svg>
|
|
||||||
<svg id="ccn-datetimepicker-panelMinute" xmlns="http://www.w3.org/2000/svg" version="1.1" preserveAspectRatio="xMidYMid" viewBox="0 0 200 200">
|
|
||||||
<circle cx="100.000000" cy="100.000000" r="100.000000" type="background"></circle>
|
|
||||||
<line x1="100" y1="100" x2="100.000000" y2="20.000000"></line>
|
|
||||||
<circle cx="100.000000" cy="20.000000" r="1em" type="symbol"></circle>
|
|
||||||
|
|
||||||
<text x="100.000000" y="20.000000">0</text>
|
|
||||||
<text x="140.000000" y="30.717968">5</text>
|
|
||||||
<text x="169.282032" y="60.000000">10</text>
|
|
||||||
<text x="180.000000" y="100.000000">15</text>
|
|
||||||
<text x="169.282032" y="140.000000">20</text>
|
|
||||||
<text x="140.000000" y="169.282032">25</text>
|
|
||||||
<text x="100.000000" y="180.000000">30</text>
|
|
||||||
<text x="60.000000" y="169.282032">35</text>
|
|
||||||
<text x="30.717968" y="140.000000">40</text>
|
|
||||||
<text x="20.000000" y="100.000000">45</text>
|
|
||||||
<text x="30.717968" y="60.000000">50</text>
|
|
||||||
<text x="60.000000" y="30.717968">55</text>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<a id="ccn-datetimepicker-btnConfirm" class="button is-success">
|
|
||||||
<span i18n-name="ccn-i18n-datetimepicker-confirm"></span>
|
|
||||||
</a>
|
|
||||||
<a id="ccn-datetimepicker-btnCancel" class="button">
|
|
||||||
<span i18n-name="ccn-i18n-datetimepicker-cancel"></span>
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<div id="ccn-displayOwnedItem-{{:uuid}}" class="collection-item card">
|
|
||||||
<div class="collection-item-words">
|
|
||||||
<p>{{>name}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-displayOwnedItem-btnHide-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-eye"></i></span></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-displayOwnedItem-btnShow-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-eye-slash"></i></span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<div id="ccn-displaySharedItem-{{:uuid}}" class="collection-item card">
|
|
||||||
<div class="collection-item-words">
|
|
||||||
<b>{{>name}}</b>
|
|
||||||
<p>
|
|
||||||
<span i18n-name="ccn-i18n-sharedItem-sharedBy"></span>
|
|
||||||
<span>{{>username}}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-displaySharedItem-btnHide-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-eye"></i></span></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-displaySharedItem-btnShow-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-eye-slash"></i></span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<nav class="navbar has-shadow is-spaced bd-navbar" role="navigation" aria-label="main navigation">
|
|
||||||
<div class="navbar-brand">
|
|
||||||
<a class="navbar-item" href="home">
|
|
||||||
<img src="/static/image/icon.png"><b style="margin:0 0 0 14px;">coconut-leaf</b>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
|
|
||||||
data-target="navbarBasicExample">
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="navbarBasicExample" class="navbar-menu">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<a i18n-name="ccn-i18n-header-nav-home" id="ccn-header-nav-home" class="navbar-item" href="/web/home"></a>
|
|
||||||
<a i18n-name="ccn-i18n-header-nav-collection" id="ccn-header-nav-collection" class="navbar-item" href="/web/collection"></a>
|
|
||||||
<a i18n-name="ccn-i18n-header-nav-calendar" id="ccn-header-nav-calendar" class="navbar-item" href="/web/calendar"></a>
|
|
||||||
<a i18n-name="ccn-i18n-header-nav-todo" id="ccn-header-nav-todo" class="navbar-item" href="/web/todo"></a>
|
|
||||||
<a i18n-name="ccn-i18n-header-nav-admin" id="ccn-header-nav-admin" class="navbar-item" href="/web/admin"></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="navbar-end">
|
|
||||||
<p id="ccn-header-user-login" class="navbar-item">
|
|
||||||
<a class="button is-primary" i18n-name="ccn-i18n-header-user-login" href="/web/login"></a>
|
|
||||||
</p>
|
|
||||||
<p id="ccn-header-user-logout" class="navbar-item">
|
|
||||||
<a class="button is-primary" i18n-name="ccn-i18n-header-user-logout"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
|
||||||
<a class="navbar-link" i18n-name="ccn-i18n-header-language"></a>
|
|
||||||
|
|
||||||
<div id="ccn-header-language" class="navbar-dropdown">
|
|
||||||
<a language="en-US" class="navbar-item">English</a>
|
|
||||||
<a language="zh-CN" class="navbar-item">简体中文</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<div id="ccn-messagebox-modal" class="modal" style="float: left; position: fixed; top: 0; bottom: 0; left: 0; right: 0;">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p id="ccn-messagebox-title" class="modal-card-title" i18n-name="ccn-i18n-messagebox-title"></p>
|
|
||||||
<button id="ccn-messagebox-btnClose" class="delete" aria-label="close"></button>
|
|
||||||
</header>
|
|
||||||
<div class="modal-card-body">
|
|
||||||
<p id="ccn-messagebox-body"></p>
|
|
||||||
</div>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button id="ccn-messagebox-btnConfirm" class="button is-success" i18n-name="ccn-i18n-messagebox-confirm"></button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<option value="{{:val}}">{{>name}}</option>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<div id="ccn-ownedItem-{{:uuid}}" class="collection-item card">
|
|
||||||
<div class="collection-item-words">
|
|
||||||
<p id="ccn-ownedItem-textName-{{:uuid}}">{{>name}}</p>
|
|
||||||
<div id="ccn-ownedItem-boxName-{{:uuid}}" class="control">
|
|
||||||
<input id="ccn-ownedItem-inputName-{{:uuid}}" class="input" type="text"></input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-ownedItem-btnEdit-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-pen"></i></span></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-ownedItem-btnShare-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-share"></i></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-ownedItem-btnDelete-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-trash"></i></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-ownedItem-btnUpdate-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<button class="button"><span class="icon is-small"><i class="fas fa-check"></i></span></button>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-ownedItem-btnCancelUpdate-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<button class="button"><span class="icon is-small"><i class="fas fa-times"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{{for renderdata}}
|
|
||||||
<div class="schedule-day container">
|
|
||||||
<div class="schedule-day-words">
|
|
||||||
<b i18n-name="ccn-i18n-universal-month-{{:month}}"></b>
|
|
||||||
<b>{{>day}}</b>
|
|
||||||
<b i18n-name="ccn-i18n-universal-week-{{:dayOfWeek}}"></b>
|
|
||||||
</div>
|
|
||||||
<div class="schedule-event-list">
|
|
||||||
{{for events}}
|
|
||||||
{{if isVisible}}
|
|
||||||
<div class="schedule-event-outter card" uuid="{{:uuid}}">
|
|
||||||
<div class="schedule-event-color" style="background: {{:color}};"></div>
|
|
||||||
<div class="schedule-event-inner">
|
|
||||||
<div class="schedule-event-words">
|
|
||||||
<p class="level-item"><b>{{>title}}</b></p>
|
|
||||||
<p class="level-item">{{>description}}</p>
|
|
||||||
<p class="level-item"><span>{{>start}}</span>-<span>{{>end}}</span></p>
|
|
||||||
{{if loopText != ""}}
|
|
||||||
<p><span class="icon is-small"><i class="fas fa-retweet"></i></span><span>{{>loopText}}</span></p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<div class="schedule-event-icon">
|
|
||||||
{{if isLocked}}
|
|
||||||
<span class="icon is-small"><i class="fas fa-lock"></i></span>
|
|
||||||
{{/if}}
|
|
||||||
{{if timezoneWarning}}
|
|
||||||
<span class="icon is-small"><i class="fas fa-globe"></i></span>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/for}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/for}}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<div id="ccn-sharingItem-{{:uuid}}" class="collection-item card">
|
|
||||||
<div class="collection-item-words">
|
|
||||||
<p>{{>username}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-sharingItem-btnDelete-{{:uuid}}" uuid="{{:uuid}}" class="collection-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-trash"></i></span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<div id="ccn-todoItem-{{:uuid}}" class="todo-item card">
|
|
||||||
<div class="todo-item-words">
|
|
||||||
<p id="ccn-todoItem-p-{{:uuid}}">{{:data}}</p>
|
|
||||||
<textarea id="ccn-todoItem-textarea-{{:uuid}}" class="textarea"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-todoItem-btnEdit-{{:uuid}}" uuid="{{:uuid}}" class="todo-item-icon control">
|
|
||||||
<button class="button"><span class="icon is-small"><i class="fas fa-pen"></i></span></button>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-todoItem-btnDelete-{{:uuid}}" uuid="{{:uuid}}" class="todo-item-icon control">
|
|
||||||
<button class="button"><span class="icon is-small"><i class="fas fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-todoItem-btnUpdate-{{:uuid}}" uuid="{{:uuid}}" class="todo-item-icon control">
|
|
||||||
<button class="button"><span class="icon is-small"><i class="fas fa-check"></i></span></button>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-todoItem-btnCancelUpdate-{{:uuid}}" uuid="{{:uuid}}" class="todo-item-icon control">
|
|
||||||
<button class="button"><span class="icon is-small"><i class="fas fa-times"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<div id="ccn-tokenItem-{{:uuid}}" class="token-item card">
|
|
||||||
<div class="token-item-words">
|
|
||||||
<b>{{>uuid}}</b>
|
|
||||||
<p>
|
|
||||||
<span i18n-name="ccn-i18n-tokenItem-ua"></span>
|
|
||||||
<span>{{>ua}}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span i18n-name="ccn-i18n-tokenItem-ip"></span>
|
|
||||||
<span>{{>ip}}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span i18n-name="ccn-i18n-tokenItem-expireOn"></span>
|
|
||||||
<span>{{>expireOn}}</span>
|
|
||||||
</p>
|
|
||||||
{{if isMe}}
|
|
||||||
<p>
|
|
||||||
<span class="icon is-small"><i class="fas fa-exclamation-triangle"></i></span>
|
|
||||||
<span i18n-name="ccn-i18n-tokenItem-isMe"></span>
|
|
||||||
</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-tokenItem-btnLogout-{{:uuid}}" uuid="{{:uuid}}" class="token-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-sign-out-alt"></i></span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<div id="ccn-userItem-{{:uuid}}" class="user-item card">
|
|
||||||
<div class="user-item-words">
|
|
||||||
<div class="control" style="display: flex; flex-flow: row; align-items: center;">
|
|
||||||
<div id="ccn-userItem-iconIsAdmin-{{:uuid}}" class="icon is-small" style="margin-right: 1rem;">
|
|
||||||
<i class="fas fa-wrench"></i>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-userItem-textName-{{:uuid}}">{{>username}}</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-userItem-boxPassword-{{:uuid}}" class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-userItem-newPassword"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-userItem-inputPassword-{{:uuid}}" class="input" type="password"></input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-userItem-boxIsAdmin-{{:uuid}}" class="field">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-userItem-inputIsAdmin-{{:uuid}}" type="checkbox"><span i18n-name="ccn-i18n-userItem-isAdmin"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-userItem-btnEdit-{{:uuid}}" uuid="{{:uuid}}" class="user-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-pen"></i></span></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-userItem-btnDelete-{{:uuid}}" uuid="{{:uuid}}" class="user-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-trash"></i></span></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-userItem-btnUpdate-{{:uuid}}" uuid="{{:uuid}}" class="user-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-check"></i></span></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-userItem-btnCancelUpdate-{{:uuid}}" uuid="{{:uuid}}" class="user-item-icon control">
|
|
||||||
<a class="button"><span class="icon is-small"><i class="fas fa-times"></i></span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-userItem" src="/static/tmpl/userItem.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-tokenItem" src="/static/tmpl/tokenItem.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/tabcontrol.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/admin.js"></script>
|
|
||||||
<link rel="stylesheet" href="/static/css/admin.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container" style="margin-top: 20px;">
|
|
||||||
<div class="tabs">
|
|
||||||
<ul>
|
|
||||||
<li id="tabcontrol-tab-1-1" class="tabcontrol-tab-1"><a
|
|
||||||
i18n-name="ccn-i18n-admin-tabcontrol-tabProfile"></a></li>
|
|
||||||
<li id="tabcontrol-tab-1-2" class="tabcontrol-tab-1"><a
|
|
||||||
i18n-name="ccn-i18n-admin-tabcontrol-tabToken"></a></li>
|
|
||||||
<li id="tabcontrol-tab-1-3" class="tabcontrol-tab-1"><a
|
|
||||||
i18n-name="ccn-i18n-admin-tabcontrol-tabUserList"></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabcontrol-panel-1-1" class="container tabcontrol-panel-1" style="margin-top: 20px;">
|
|
||||||
<h1 class="title" i18n-name="ccn-i18n-admin-changePassword"></h1>
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-admin-profile-inputPassword" class="input" type="password">
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<a id="ccn-admin-profile-btnChangePassword" class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-key"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabcontrol-panel-1-2" class="container tabcontrol-panel-1" style="margin-top: 20px;">
|
|
||||||
<h1 class="title" i18n-name="ccn-i18n-admin-manageToken"></h1>
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-admin-manageToken-desc"></h2>
|
|
||||||
<div id="ccn-admin-tokenList-btnRefresh" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-sync"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-admin-tokenList" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabcontrol-panel-1-3" class="container tabcontrol-panel-1" style="margin-top: 20px;">
|
|
||||||
<h1 class="title" i18n-name="ccn-i18n-admin-userList"></h1>
|
|
||||||
<div class="control-list">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-admin-userList-inputUsername" class="input" type="text">
|
|
||||||
</div>
|
|
||||||
<div id="ccn-admin-userList-btnAdd" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-plus"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-admin-userList-btnRefresh" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-sync"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-admin-userList" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript"
|
|
||||||
src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-datetimepicker" src="/static/tmpl/datetimepicker.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-calendarItem" src="/static/tmpl/calendarItem.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-scheduleItem" src="/static/tmpl/scheduleItem.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-displayOwnedItem" src="/static/tmpl/displayOwnedItem.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-displaySharedItem" src="/static/tmpl/displaySharedItem.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/datetime.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/tabcontrol.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/datetimepicker.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/calendar.js"></script>
|
|
||||||
<link rel="stylesheet" href="/static/css/calendar.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/datetimepicker.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container" style="margin-top: 20px;">
|
|
||||||
<div class="tabs">
|
|
||||||
<ul>
|
|
||||||
<li id="tabcontrol-tab-1-1" class="tabcontrol-tab-1"><a
|
|
||||||
i18n-name="ccn-i18n-calendar-tabcontrol-tabCalendar"></a></li>
|
|
||||||
<li id="tabcontrol-tab-1-2" class="tabcontrol-tab-1"><a
|
|
||||||
i18n-name="ccn-i18n-calendar-tabcontrol-tabCollection"></a></li>
|
|
||||||
<li id="tabcontrol-tab-1-3" class="tabcontrol-tab-1"><a
|
|
||||||
i18n-name="ccn-i18n-calendar-tabcontrol-tabDisplay"></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabcontrol-panel-1-1" class="container tabcontrol-panel-1" style="margin-top: 20px;">
|
|
||||||
<nav class="level is-mobile">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-calendar-calendar-btnPrevMonth" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-calendar-calendar-btnJump" class="button" datetimepicker="1">
|
|
||||||
<span id="ccn-calendar-calendar-textMonth"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="level-right">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-calendar-calendar-btnNextMonth" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<nav class="level is-mobile">
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-calendar-calendar-btnToday" i18n-name="ccn-i18n-calendar-calendar-today" class="button is-info"></a>
|
|
||||||
</div>
|
|
||||||
<div class="level-item control">
|
|
||||||
<a id="ccn-calendar-calendar-btnAdd" i18n-name="ccn-i18n-calendar-calendar-add" class="button is-primary"></a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="ccn-calendar-calendarBody" class="card" style="padding: 1.25rem; display: flex; flex-flow: column;">
|
|
||||||
<div style="margin: 0 0 0.75em 0;">
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-1"></b></div>
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-2"></b></div>
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-3"></b></div>
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-4"></b></div>
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-5"></b></div>
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-6" style="color: red;"></b></div>
|
|
||||||
<div><b i18n-name="ccn-i18n-universal-week-7" style="color: red;"></b></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" style="padding: 1.25rem; display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
<h1 i18n-name="ccn-i18n-calendar-calendar-scheduleList" class="title"></h1>
|
|
||||||
|
|
||||||
<div id="ccn-calendar-scheduleList">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabcontrol-panel-1-2" class="container tabcontrol-panel-1" style="margin-top: 20px;">
|
|
||||||
<div id="ccn-calendar-collection-btnRefresh" class="control" style="margin: 0.75rem;">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-sync"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1 i18n-name="ccn-i18n-calendar-owned-list" class="title"></h1>
|
|
||||||
<div id="ccn-calendar-ownedList" style="display: flex; flex-flow: column; margin-top: 1.25rem; margin-bottom: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
<h1 i18n-name="ccn-i18n-calendar-shared-list" class="title"></h1>
|
|
||||||
<div id="ccn-calendar-sharedList" style="display: flex; flex-flow: column; margin-top: 1.25rem; margin-bottom: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tabcontrol-panel-1-3" class="container tabcontrol-panel-1" style="margin-top: 20px;">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-calendar-display-firstDayOfWeek"></label>
|
|
||||||
<div class="control">
|
|
||||||
<div class="select">
|
|
||||||
<select id="ccn-calendar-display-firstDayOfWeek">
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-1"></option>
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-2"></option>
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-3"></option>
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-4"></option>
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-5"></option>
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-6"></option>
|
|
||||||
<option i18n-name="ccn-i18n-universal-week-7"></option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-calendar-display-subcalendar"></label>
|
|
||||||
<div class="control">
|
|
||||||
<div class="select">
|
|
||||||
<select id="ccn-calendar-display-subcalendar">
|
|
||||||
<option i18n-name="ccn-i18n-calendar-display-subcalendar-chineseLunisolarCalendar"></option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript"
|
|
||||||
src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-ownedItem" src="/static/tmpl/ownedItem.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-sharingItem" src="/static/tmpl/sharingItem.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/tabcontrol.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/collection.js"></script>
|
|
||||||
<link rel="stylesheet" href="/static/css/collection.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
|
|
||||||
<div class="container" style="display: flex; flex-flow: column;">
|
|
||||||
<h1 i18n-name="ccn-i18n-collection-owned-list" class="title"></h1>
|
|
||||||
<div class="control-list">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-collection-owned-inputAdd" class="input" type="text">
|
|
||||||
</div>
|
|
||||||
<div id="ccn-collection-owned-btnAdd" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-plus"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-collection-owned-btnRefresh" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-sync"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-collection-ownedList" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-collection-sharing-container" class="container" style="display: flex; flex-flow: column;">
|
|
||||||
<h1 i18n-name="ccn-i18n-collection-sharing-list" class="title"></h1>
|
|
||||||
<label class="label"><span i18n-name="ccn-i18n-collection-sharing-editing"></span>
|
|
||||||
<span id="ccn-collection-sharing-sharingEditing"></span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="control-list">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-collection-sharing-inputAdd" class="input" type="text">
|
|
||||||
</div>
|
|
||||||
<div id="ccn-collection-sharing-btnAdd" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-plus"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-collection-sharing-btnRefresh" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-sync"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-collection-sharingList" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript"
|
|
||||||
src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<!-- if is empty, mean add, otherwise, it is a uuid-->
|
|
||||||
<meta name="uuid" content="{{uuidPath}}">
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-datetimepicker" src="/static/tmpl/datetimepicker.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-optionItem" src="/static/tmpl/optionItem.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/datetime.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/datetimepicker.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/event.js"></script>
|
|
||||||
<link rel="stylesheet" href="/static/css/event.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/datetimepicker.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- add is-active in class to show this-->
|
|
||||||
<div id="ccn-event-eventFormBody" class="container" style="margin-top: 20px;">
|
|
||||||
<h1 i18n-name="ccn-i18n-event-header" class="title"></h1>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-title"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-inputTitle" class="input" type="text">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-description"></label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea id="ccn-event-inputDescription" class="textarea"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-color"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-inputColor" class="input" type="color">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-collection"></label>
|
|
||||||
<div class="control">
|
|
||||||
<div class="select">
|
|
||||||
<select id="ccn-event-inputCollection"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-event-startDateTime"></h2>
|
|
||||||
<a id="ccn-event-btnStartDateTime" class="button" datetimepicker="1">
|
|
||||||
<span id="ccn-event-btnStartDateTime-text"></span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-event-endDateTime"></h2>
|
|
||||||
<div class="control-list">
|
|
||||||
<div class="control">
|
|
||||||
<a id="ccn-event-btnSpot" class="button is-link" i18n-name="ccn-i18n-event-btnSpot"></a>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<a id="ccn-event-btnFullDay" class="button is-link" i18n-name="ccn-i18n-event-btnFullDay"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a id="ccn-event-btnEndDateTime" class="button" datetimepicker="2">
|
|
||||||
<span id="ccn-event-btnEndDateTime-text"></span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-event-loop"></h2>
|
|
||||||
<div class="control-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-radioLoopNever" type="radio" name="loop-method">
|
|
||||||
<span i18n-name="ccn-i18n-event-loop-never"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-radioLoopDay" type="radio" name="loop-method">
|
|
||||||
<span i18n-name="ccn-i18n-event-loop-day"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-radioLoopWeek" type="radio" name="loop-method">
|
|
||||||
<span i18n-name="ccn-i18n-event-loop-week"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-radioLoopMonth" type="radio" name="loop-method">
|
|
||||||
<span i18n-name="ccn-i18n-event-loop-month"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-radioLoopYear" type="radio" name="loop-method">
|
|
||||||
<span i18n-name="ccn-i18n-event-loop-year"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-event-boxLoopDay">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-loopDay-span"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-loopDay-inputSpan" class="input spanpicker" type="number">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-event-boxLoopWeek">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-loopWeek-span"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-loopWeek-inputSpan" class="input spanpicker" type="number">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-loopWeek-option"></label>
|
|
||||||
<div class="control-list">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check1" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-1"></span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check2" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-2"></span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check3" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-3"></span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check4" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-4"></span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check5" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-5"></span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check6" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-6"></span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input id="ccn-event-loopWeek-check7" type="checkbox">
|
|
||||||
<span i18n-name="ccn-i18n-universal-week-7"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-event-boxLoopMonth">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-loopMonth-span"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-loopMonth-inputSpan" class="input spanpicker" type="number">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-loopWeek-option"></label>
|
|
||||||
<div class="control-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopMonth-radioA" type="radio" name="month-loop-method">
|
|
||||||
<span id="ccn-event-loopMonth-textA"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopMonth-radioB" type="radio" name="month-loop-method">
|
|
||||||
<span id="ccn-event-loopMonth-textB"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopMonth-radioC" type="radio" name="month-loop-method">
|
|
||||||
<span id="ccn-event-loopMonth-textC"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopMonth-radioD" type="radio" name="month-loop-method">
|
|
||||||
<span id="ccn-event-loopMonth-textD"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-event-boxLoopYear">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-event-loopYear-span"></label>
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-loopYear-inputSpan" class="input spanpicker" type="number">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="ccn-event-boxLoopStop" class="section">
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-event-loopStop"></h2>
|
|
||||||
<div class="control-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopStop-radioForever" type="radio" name="loop-end">
|
|
||||||
<span i18n-name="ccn-i18n-event-loopStop-forever"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopStop-radioDateTime" type="radio" name="loop-end">
|
|
||||||
<span i18n-name="ccn-i18n-event-loopStop-datetime"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-loopStop-radioTimes" type="radio" name="loop-end">
|
|
||||||
<span i18n-name="ccn-i18n-event-loopStop-times"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-event-boxLoopStopDateTime">
|
|
||||||
<a id="ccn-event-btnLoopStopDateTime" class="button" datetimepicker="3">
|
|
||||||
<span id="ccn-event-btnLoopStopDateTime-text"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-event-boxLoopStopTimes">
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input id="ccn-event-loopStop-inputTimes" class="input spanpicker" type="number">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="ccn-event-boxStrictMode" class="section">
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-event-strictMode-title"></h2>
|
|
||||||
<p i18n-name="ccn-i18n-event-strictMode-warning"></p>
|
|
||||||
<div class="control-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-strictMode-radioStrict" type="radio" name="timezone">
|
|
||||||
<span i18n-name="ccn-i18n-event-strictMode-strict"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-strictMode-radioRough" type="radio" name="timezone">
|
|
||||||
<span i18n-name="ccn-i18n-event-strictMode-rough"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="ccn-event-boxTimezone" class="section">
|
|
||||||
<h2 class="subtitle" i18n-name="ccn-i18n-event-timezone-title"></h2>
|
|
||||||
<p i18n-name="ccn-i18n-event-timezone-warning"></p>
|
|
||||||
<div class="control-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-timezone-radioKeep" type="radio" name="timezone">
|
|
||||||
<span i18n-name="ccn-i18n-event-timezone-keep"></span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input id="ccn-event-timezone-radioReplace" type="radio" name="timezone">
|
|
||||||
<span i18n-name="ccn-i18n-event-timezone-replace"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<div class="control-list">
|
|
||||||
<a id="ccn-event-btnSubmit" class="button is-success" i18n-name="ccn-i18n-event-btnSubmit"></a>
|
|
||||||
<a id="ccn-event-btnCancel" class="button" i18n-name="ccn-i18n-event-btnCancel"></a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/home.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container" style="margin-top: 1.25rem;">
|
|
||||||
<article i18n-name="ccn-i18n-home-desc">
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html style="height: 100%; overflow: hidden;">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/login.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="height: 100%; display: flex; flex-flow: column;">
|
|
||||||
|
|
||||||
<div style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center;">
|
|
||||||
<div class="card" style="padding: 1.25rem;">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-login-form-username"></label>
|
|
||||||
<div class="control has-icons-left has-icons-right">
|
|
||||||
<input id="ccn-login-form-username" class="input" type="text">
|
|
||||||
<span class="icon is-small is-left">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" i18n-name="ccn-i18n-login-form-password"></label>
|
|
||||||
<p class="control has-icons-left">
|
|
||||||
<input id="ccn-login-form-password" class="input" type="password">
|
|
||||||
<span class="icon is-small is-left">
|
|
||||||
<i class="fas fa-lock"></i>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control">
|
|
||||||
<button id="ccn-login-form-login" class="button is-primary" i18n-name="ccn-i18n-login-form-login"></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title id="ccn-pageName"></title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.12.1/js/all.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery-i18n-properties@1.2.7/jquery.i18n.properties.js"></script>
|
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsrender@1.0.10/jsrender.min.js"></script>
|
|
||||||
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-headerNav" src="/static/tmpl/headerNav.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-messagebox" src="/static/tmpl/messagebox.tmpl"></script>
|
|
||||||
<script type="text/x-jsrender" id="jsrender-tmpl-todoItem" src="/static/tmpl/todoItem.tmpl"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/localStorageAssist.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/i18n.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/utils.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/api.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/template.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/headerNav.js"></script>
|
|
||||||
<script type="text/javascript" src="/static/js/messagebox.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/js/page/todo.js"></script>
|
|
||||||
<link rel="stylesheet" href="/static/css/todo.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
<h1 i18n-name="ccn-i18n-todo-todoList" class="title"></h1>
|
|
||||||
<div class="control-list">
|
|
||||||
<div id="ccn-todo-btnAdd" class="control">
|
|
||||||
<a class="button is-primary"><span class="icon is-small"><i class="fas fa-plus"></i></span></a>
|
|
||||||
</div>
|
|
||||||
<div id="ccn-todo-btnRefresh" class="control">
|
|
||||||
<a class="button is-primary">
|
|
||||||
<span class="icon is-small"><i class="fas fa-sync"></i></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ccn-todo-todoList" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
+45
-14
@@ -1,27 +1,43 @@
|
|||||||
# coleaf-frontend
|
# coleaf-frontend
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
The web frontend of **coconut-leaf** — a light, self-hosted, multi-account calendar system.
|
||||||
|
This package is a from-scratch rewrite of the legacy JavaScript + jQuery frontend (`frontend-legacy/`) on a modern toolchain.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Tech Stack
|
||||||
|
|
||||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
- **Framework:** Vue 3 (`<script setup>` + TypeScript)
|
||||||
|
- **Build tool:** Vite
|
||||||
|
- **State management:** Pinia (with `pinia-plugin-persistedstate`)
|
||||||
|
- **Routing:** Vue Router (navigation guards + per-route window titles)
|
||||||
|
- **Internationalization:** vue-i18n (`en-US` / `zh-CN`, message files under `src/locales/`)
|
||||||
|
- **Styling:** Bulma 0.9 (imported via SCSS) + Font Awesome icons
|
||||||
|
- **Linting:** oxlint + ESLint
|
||||||
|
|
||||||
## Recommended Browser Setup
|
Requires Node `^20.19` or `>=22.12` (see `package.json` → `engines`).
|
||||||
|
|
||||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
## Project Layout
|
||||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
|
||||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
|
||||||
- Firefox:
|
|
||||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
|
||||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
|
||||||
|
|
||||||
## Type Support for `.vue` Imports in TS
|
```text
|
||||||
|
src/
|
||||||
|
├── api/ # backend client (fetch-based apiWrapper + typed endpoints)
|
||||||
|
├── components/ # reusable UI (calendar/, collection/, todo/, admin/, DateTimePicker, MessageBox)
|
||||||
|
├── locales/ # i18n messages (enUS.ts, zhCN.ts)
|
||||||
|
├── router/ # route table + beforeEach guard (auth check + document.title)
|
||||||
|
├── stores/ # Pinia stores (token, language)
|
||||||
|
├── utils/ # datetime recurrence engine, i18n bootstrap, helpers
|
||||||
|
└── views/ # page-level views (Home, Login, Collection, Todo, Calendar, CalendarEvent, Admin, NotFound)
|
||||||
|
```
|
||||||
|
|
||||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
## Development Server
|
||||||
|
|
||||||
## Customize configuration
|
`vite.config.ts` is preconfigured:
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
- **`base: '/web/'`** — matches the `/web` prefix routed by the production Nginx.
|
||||||
|
- **Dev port:** `5173`, opens `/web/` on start.
|
||||||
|
- **API proxy:** `/api/<path>` → `http://127.0.0.1:8848/<path>` (the local
|
||||||
|
backend), with the `/api` prefix stripped on the fly.
|
||||||
|
|
||||||
|
Start the backend (listening on `127.0.0.1:8848`) before launching the frontend.
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
@@ -46,3 +62,18 @@ pnpm build
|
|||||||
```sh
|
```sh
|
||||||
pnpm lint
|
pnpm lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Internationalization
|
||||||
|
|
||||||
|
- Translation strings live in `src/locales/enUS.ts` and `src/locales/zhCN.ts`
|
||||||
|
(keys omit the legacy `ccn-i18n-` prefix).
|
||||||
|
- The active language is held in the Pinia `language` store (persisted under
|
||||||
|
the `ccn-i18n` localStorage key) and synced to `i18n.global.locale` via a
|
||||||
|
watcher in `src/App.vue`.
|
||||||
|
- Switch languages from the navbar dropdown; all visible text — including the
|
||||||
|
window title (driven by each route's `meta.title`) — updates immediately.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Source code is licensed under [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.html).
|
||||||
|
See the repository root for details.
|
||||||
|
|||||||
@@ -20,4 +20,21 @@ export default defineConfigWithVueTs(
|
|||||||
vueTsConfigs.recommended,
|
vueTsConfigs.recommended,
|
||||||
|
|
||||||
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
||||||
|
|
||||||
|
// 该前端由旧前端迁移而来,不得已使用 any,因此关闭此规则
|
||||||
|
{
|
||||||
|
name: 'app/legacy-no-explicit-any',
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// views/ 目录下为路由页面级组件,非通用组件,不会与 HTML 原生元素冲突,因此关闭多词组件名检查
|
||||||
|
{
|
||||||
|
name: 'app/views-single-word',
|
||||||
|
files: ['src/views/**/*.vue'],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.2.0",
|
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||||
"axios": "1.14.0",
|
|
||||||
"bulma": "0.9.1",
|
"bulma": "0.9.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
|
"vue-i18n": "^11.4.7",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+56
-191
@@ -17,9 +17,6 @@ importers:
|
|||||||
'@fortawesome/vue-fontawesome':
|
'@fortawesome/vue-fontawesome':
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3))
|
version: 3.2.0(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.33(typescript@6.0.3))
|
||||||
axios:
|
|
||||||
specifier: 1.14.0
|
|
||||||
version: 1.14.0
|
|
||||||
bulma:
|
bulma:
|
||||||
specifier: 0.9.1
|
specifier: 0.9.1
|
||||||
version: 0.9.1
|
version: 0.9.1
|
||||||
@@ -32,6 +29,9 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.32
|
specifier: ^3.5.32
|
||||||
version: 3.5.33(typescript@6.0.3)
|
version: 3.5.33(typescript@6.0.3)
|
||||||
|
vue-i18n:
|
||||||
|
specifier: ^11.4.7
|
||||||
|
version: 11.4.7(vue@3.5.33(typescript@6.0.3))
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3))
|
version: 5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3))
|
||||||
@@ -301,6 +301,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@intlify/core-base@11.4.7':
|
||||||
|
resolution: {integrity: sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==}
|
||||||
|
engines: {node: '>= 22'}
|
||||||
|
|
||||||
|
'@intlify/devtools-types@11.4.7':
|
||||||
|
resolution: {integrity: sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==}
|
||||||
|
engines: {node: '>= 22'}
|
||||||
|
|
||||||
|
'@intlify/message-compiler@11.4.7':
|
||||||
|
resolution: {integrity: sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==}
|
||||||
|
engines: {node: '>= 22'}
|
||||||
|
|
||||||
|
'@intlify/shared@11.4.7':
|
||||||
|
resolution: {integrity: sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==}
|
||||||
|
engines: {node: '>= 22'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
@@ -782,6 +798,9 @@ packages:
|
|||||||
'@vue/compiler-ssr@3.5.33':
|
'@vue/compiler-ssr@3.5.33':
|
||||||
resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==}
|
resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@6.6.4':
|
||||||
|
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||||
|
|
||||||
'@vue/devtools-api@7.7.9':
|
'@vue/devtools-api@7.7.9':
|
||||||
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
||||||
|
|
||||||
@@ -879,12 +898,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==}
|
resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
asynckit@0.4.0:
|
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
|
||||||
|
|
||||||
axios@1.14.0:
|
|
||||||
resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==}
|
|
||||||
|
|
||||||
balanced-match@4.0.4:
|
balanced-match@4.0.4:
|
||||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@@ -920,10 +933,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
|
||||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
caniuse-lite@1.0.30001791:
|
caniuse-lite@1.0.30001791:
|
||||||
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
|
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
|
||||||
|
|
||||||
@@ -935,10 +944,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
|
||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
|
|
||||||
confbox@0.1.8:
|
confbox@0.1.8:
|
||||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||||
|
|
||||||
@@ -991,18 +996,10 @@ packages:
|
|||||||
defu@6.1.7:
|
defu@6.1.7:
|
||||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||||
|
|
||||||
delayed-stream@1.0.0:
|
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
|
||||||
engines: {node: '>=0.4.0'}
|
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
electron-to-chromium@1.5.344:
|
electron-to-chromium@1.5.344:
|
||||||
resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==}
|
resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==}
|
||||||
|
|
||||||
@@ -1013,22 +1010,6 @@ packages:
|
|||||||
error-stack-parser-es@1.0.5:
|
error-stack-parser-es@1.0.5:
|
||||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||||
|
|
||||||
es-define-property@1.0.1:
|
|
||||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-errors@1.3.0:
|
|
||||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
|
||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-set-tostringtag@2.1.0:
|
|
||||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
escalade@3.2.0:
|
escalade@3.2.0:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1148,39 +1129,15 @@ packages:
|
|||||||
flatted@3.4.2:
|
flatted@3.4.2:
|
||||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||||
|
|
||||||
follow-redirects@1.16.0:
|
|
||||||
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
|
||||||
engines: {node: '>=4.0'}
|
|
||||||
peerDependencies:
|
|
||||||
debug: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
debug:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
form-data@4.0.5:
|
|
||||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
function-bind@1.1.2:
|
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
|
||||||
|
|
||||||
gensync@1.0.0-beta.2:
|
gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
get-proto@1.0.1:
|
|
||||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1189,22 +1146,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
gopd@1.2.0:
|
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
has-symbols@1.1.0:
|
|
||||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
has-tostringtag@1.0.2:
|
|
||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
hasown@2.0.3:
|
|
||||||
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
@@ -1395,10 +1336,6 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
memorystream@0.3.1:
|
memorystream@0.3.1:
|
||||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -1411,14 +1348,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
mime-db@1.52.0:
|
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
mime-types@2.1.35:
|
|
||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@@ -1571,10 +1500,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
proxy-from-env@2.1.0:
|
|
||||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1807,6 +1732,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
|
||||||
|
vue-i18n@11.4.7:
|
||||||
|
resolution: {integrity: sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==}
|
||||||
|
engines: {node: '>= 22'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.0.0
|
||||||
|
|
||||||
vue-router@5.0.6:
|
vue-router@5.0.6:
|
||||||
resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==}
|
resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2143,6 +2074,24 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@intlify/core-base@11.4.7':
|
||||||
|
dependencies:
|
||||||
|
'@intlify/devtools-types': 11.4.7
|
||||||
|
'@intlify/message-compiler': 11.4.7
|
||||||
|
'@intlify/shared': 11.4.7
|
||||||
|
|
||||||
|
'@intlify/devtools-types@11.4.7':
|
||||||
|
dependencies:
|
||||||
|
'@intlify/core-base': 11.4.7
|
||||||
|
'@intlify/shared': 11.4.7
|
||||||
|
|
||||||
|
'@intlify/message-compiler@11.4.7':
|
||||||
|
dependencies:
|
||||||
|
'@intlify/shared': 11.4.7
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
'@intlify/shared@11.4.7': {}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -2551,6 +2500,8 @@ snapshots:
|
|||||||
'@vue/compiler-dom': 3.5.33
|
'@vue/compiler-dom': 3.5.33
|
||||||
'@vue/shared': 3.5.33
|
'@vue/shared': 3.5.33
|
||||||
|
|
||||||
|
'@vue/devtools-api@6.6.4': {}
|
||||||
|
|
||||||
'@vue/devtools-api@7.7.9':
|
'@vue/devtools-api@7.7.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-kit': 7.7.9
|
'@vue/devtools-kit': 7.7.9
|
||||||
@@ -2669,16 +2620,6 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.2
|
||||||
ast-kit: 2.2.0
|
ast-kit: 2.2.0
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
|
||||||
|
|
||||||
axios@1.14.0:
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: 1.16.0
|
|
||||||
form-data: 4.0.5
|
|
||||||
proxy-from-env: 2.1.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.23: {}
|
baseline-browser-mapping@2.10.23: {}
|
||||||
@@ -2709,11 +2650,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
run-applescript: 7.1.0
|
run-applescript: 7.1.0
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
|
||||||
dependencies:
|
|
||||||
es-errors: 1.3.0
|
|
||||||
function-bind: 1.1.2
|
|
||||||
|
|
||||||
caniuse-lite@1.0.30001791: {}
|
caniuse-lite@1.0.30001791: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
@@ -2725,10 +2661,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 5.0.0
|
readdirp: 5.0.0
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
|
||||||
dependencies:
|
|
||||||
delayed-stream: 1.0.0
|
|
||||||
|
|
||||||
confbox@0.1.8: {}
|
confbox@0.1.8: {}
|
||||||
|
|
||||||
confbox@0.2.4: {}
|
confbox@0.2.4: {}
|
||||||
@@ -2766,37 +2698,14 @@ snapshots:
|
|||||||
|
|
||||||
defu@6.1.7: {}
|
defu@6.1.7: {}
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers: 1.0.2
|
|
||||||
es-errors: 1.3.0
|
|
||||||
gopd: 1.2.0
|
|
||||||
|
|
||||||
electron-to-chromium@1.5.344: {}
|
electron-to-chromium@1.5.344: {}
|
||||||
|
|
||||||
entities@7.0.1: {}
|
entities@7.0.1: {}
|
||||||
|
|
||||||
error-stack-parser-es@1.0.5: {}
|
error-stack-parser-es@1.0.5: {}
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
|
||||||
dependencies:
|
|
||||||
es-errors: 1.3.0
|
|
||||||
|
|
||||||
es-set-tostringtag@2.1.0:
|
|
||||||
dependencies:
|
|
||||||
es-errors: 1.3.0
|
|
||||||
get-intrinsic: 1.3.0
|
|
||||||
has-tostringtag: 1.0.2
|
|
||||||
hasown: 2.0.3
|
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
@@ -2931,41 +2840,11 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.4.2: {}
|
flatted@3.4.2: {}
|
||||||
|
|
||||||
follow-redirects@1.16.0: {}
|
|
||||||
|
|
||||||
form-data@4.0.5:
|
|
||||||
dependencies:
|
|
||||||
asynckit: 0.4.0
|
|
||||||
combined-stream: 1.0.8
|
|
||||||
es-set-tostringtag: 2.1.0
|
|
||||||
hasown: 2.0.3
|
|
||||||
mime-types: 2.1.35
|
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers: 1.0.2
|
|
||||||
es-define-property: 1.0.1
|
|
||||||
es-errors: 1.3.0
|
|
||||||
es-object-atoms: 1.1.1
|
|
||||||
function-bind: 1.1.2
|
|
||||||
get-proto: 1.0.1
|
|
||||||
gopd: 1.2.0
|
|
||||||
has-symbols: 1.1.0
|
|
||||||
hasown: 2.0.3
|
|
||||||
math-intrinsics: 1.1.0
|
|
||||||
|
|
||||||
get-proto@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
dunder-proto: 1.0.1
|
|
||||||
es-object-atoms: 1.1.1
|
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -2974,18 +2853,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
|
||||||
|
|
||||||
has-symbols@1.1.0: {}
|
|
||||||
|
|
||||||
has-tostringtag@1.0.2:
|
|
||||||
dependencies:
|
|
||||||
has-symbols: 1.1.0
|
|
||||||
|
|
||||||
hasown@2.0.3:
|
|
||||||
dependencies:
|
|
||||||
function-bind: 1.1.2
|
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
@@ -3121,8 +2988,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
|
||||||
|
|
||||||
memorystream@0.3.1: {}
|
memorystream@0.3.1: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@@ -3132,12 +2997,6 @@ snapshots:
|
|||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.2
|
picomatch: 2.3.2
|
||||||
|
|
||||||
mime-db@1.52.0: {}
|
|
||||||
|
|
||||||
mime-types@2.1.35:
|
|
||||||
dependencies:
|
|
||||||
mime-db: 1.52.0
|
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.5
|
brace-expansion: 5.0.5
|
||||||
@@ -3289,8 +3148,6 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
proxy-from-env@2.1.0: {}
|
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
@@ -3519,6 +3376,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
vue-i18n@11.4.7(vue@3.5.33(typescript@6.0.3)):
|
||||||
|
dependencies:
|
||||||
|
'@intlify/core-base': 11.4.7
|
||||||
|
'@intlify/devtools-types': 11.4.7
|
||||||
|
'@intlify/shared': 11.4.7
|
||||||
|
'@vue/devtools-api': 6.6.4
|
||||||
|
vue: 3.5.33(typescript@6.0.3)
|
||||||
|
|
||||||
vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)):
|
vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.1
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 133 KiB |
@@ -21,6 +21,18 @@ div.paperbox-item-words {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 多行卡片样式(作用于管理员用户列表项、会话令牌项等内部需要纵向堆叠多个字段的卡片)
|
||||||
|
div.paperbox-item-multiline-words {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
div.paperbox-item-icon {
|
div.paperbox-item-icon {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-11
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useLanguageStore } from './stores/language';
|
import { useLanguageStore } from './stores/language';
|
||||||
import { useTokenStore } from './stores/token';
|
import { useTokenStore } from './stores/token';
|
||||||
import MessageBox from '@/components/MessageBox.vue';
|
import MessageBox from '@/components/MessageBox.vue';
|
||||||
import { logout as apiCommonLogout } from './api/common';
|
import { logout as apiCommonLogout } from './api/common';
|
||||||
import { goToHome } from '@/router';
|
import { goToHome } from '@/router';
|
||||||
|
import { i18n, Language } from './utils/i18n';
|
||||||
|
|
||||||
const language = useLanguageStore();
|
const language = useLanguageStore();
|
||||||
const token = useTokenStore();
|
const token = useTokenStore();
|
||||||
@@ -13,6 +14,12 @@ const isBurgerActive = ref<boolean>(false);
|
|||||||
|
|
||||||
const messagebox = ref<InstanceType<typeof MessageBox>>();
|
const messagebox = ref<InstanceType<typeof MessageBox>>();
|
||||||
|
|
||||||
|
// Sync the vue-i18n locale with the persisted language store (immediately, so
|
||||||
|
// the very first render uses the saved language; and on every later change).
|
||||||
|
watch(() => language.language, (lang) => {
|
||||||
|
i18n.global.locale.value = lang === Language.English ? 'en-US' : 'zh-CN';
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
const tokenStore = useTokenStore();
|
const tokenStore = useTokenStore();
|
||||||
const rv = await apiCommonLogout(tokenStore.currentToken);
|
const rv = await apiCommonLogout(tokenStore.currentToken);
|
||||||
@@ -24,7 +31,7 @@ const logout = async () => {
|
|||||||
goToHome();
|
goToHome();
|
||||||
} else {
|
} else {
|
||||||
// Show logout error.
|
// Show logout error.
|
||||||
messagebox.value?.show("Fail to logout due to unknow reason. Consider refreshing page to solve problem.");
|
messagebox.value?.show(i18n.global.t('js-fail-logout'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,24 +62,23 @@ const toggleBurger = () => {
|
|||||||
|
|
||||||
<div id="coleaf-navbar" class="navbar-menu" :class="{ 'is-active': isBurgerActive }">
|
<div id="coleaf-navbar" class="navbar-menu" :class="{ 'is-active': isBurgerActive }">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<router-link class="navbar-item" to="/home">Home</router-link>
|
<router-link class="navbar-item" to="/home">{{ $t('header-nav-home') }}</router-link>
|
||||||
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/collection">Collection</router-link>
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/collection">{{ $t('header-nav-collection') }}</router-link>
|
||||||
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/calendar">Calendar</router-link>
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/calendar">{{ $t('header-nav-calendar') }}</router-link>
|
||||||
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/todo">Todo</router-link>
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/todo">{{ $t('header-nav-todo') }}</router-link>
|
||||||
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/admin">Admin</router-link>
|
<router-link v-if="token.isLoggedIn" class="navbar-item" to="/admin">{{ $t('header-nav-admin') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<p class="navbar-item">
|
<p class="navbar-item">
|
||||||
<router-link v-if="!token.isLoggedIn" class="button is-primary" to="/login">Login</router-link>
|
<router-link v-if="!token.isLoggedIn" class="button is-primary" to="/login">{{ $t('header-user-login') }}</router-link>
|
||||||
</p>
|
</p>
|
||||||
<p class="navbar-item">
|
<p class="navbar-item">
|
||||||
<a v-if="token.isLoggedIn" class="button is-primary" @click="logout">Logout</a>
|
<a v-if="token.isLoggedIn" class="button is-primary" @click="logout">{{ $t('header-user-logout') }}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a v-if="language.isEnglish" class="navbar-link">English</a>
|
<a class="navbar-link">{{ $t('header-language') }}</a>
|
||||||
<a v-else-if="language.isSimplifiedChinese" class="navbar-link">简体中文</a>
|
|
||||||
|
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
<a @click="language.changeToEnglish()" class="navbar-item">English</a>
|
<a @click="language.changeToEnglish()" class="navbar-item">English</a>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { i18n } from '@/utils/i18n';
|
||||||
|
|
||||||
const isVisible = ref(false);
|
const isVisible = ref(false);
|
||||||
const title = ref<string>("");
|
const title = ref<string>("");
|
||||||
@@ -10,7 +11,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const show = (_content: string, _title?: string) => {
|
const show = (_content: string, _title?: string) => {
|
||||||
title.value = _title ?? "Notification";
|
title.value = _title ?? i18n.global.t('messagebox-title');
|
||||||
content.value = _content;
|
content.value = _content;
|
||||||
isVisible.value = true;
|
isVisible.value = true;
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,7 @@ defineExpose({
|
|||||||
<p>{{ content }}</p>
|
<p>{{ content }}</p>
|
||||||
</div>
|
</div>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button class="button is-success" @click="ok">OK</button>
|
<button class="button is-success" @click="ok">{{ $t('messagebox-confirm') }}</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
uuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
ua: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
ip: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expireOn: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isMe: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'delete', uuid: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const deleteItem = () => {
|
||||||
|
emit('delete', props.uuid);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="paperbox-item card">
|
||||||
|
<div class="paperbox-item-multiline-words">
|
||||||
|
<b>{{ uuid }}</b>
|
||||||
|
<p>
|
||||||
|
<span>{{ $t('tokenItem-ua') }}</span>
|
||||||
|
<span>{{ ua }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>{{ $t('tokenItem-ip') }}</span>
|
||||||
|
<span>{{ ip }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>{{ $t('tokenItem-expireOn') }}</span>
|
||||||
|
<span>{{ expireOn }}</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="isMe">
|
||||||
|
<span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-exclamation-triangle"></font-awesome-icon></span>
|
||||||
|
<span>{{ $t('tokenItem-isMe') }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" @click="deleteItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-sign-out-alt"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isAdmin: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'delete', username: string): void
|
||||||
|
(e: 'update', username: string, newPassword: string, newIsAdmin: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isEditing = ref<boolean>(false);
|
||||||
|
const editingPassword = ref<string>("");
|
||||||
|
const editingIsAdmin = ref<boolean>(false);
|
||||||
|
|
||||||
|
const editItem = () => {
|
||||||
|
// copy isAdmin to checkbox and clean password box
|
||||||
|
editingIsAdmin.value = props.isAdmin;
|
||||||
|
editingPassword.value = "";
|
||||||
|
// switch to edit mode
|
||||||
|
isEditing.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = () => {
|
||||||
|
emit('delete', props.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItem = () => {
|
||||||
|
const new_password = editingPassword.value;
|
||||||
|
const new_isAdmin = editingIsAdmin.value;
|
||||||
|
// clean editing state
|
||||||
|
editingPassword.value = "";
|
||||||
|
// switch to normal mode
|
||||||
|
isEditing.value = false;
|
||||||
|
emit('update', props.username, new_password, new_isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelUpdateItem = () => {
|
||||||
|
// clean editing state
|
||||||
|
editingPassword.value = "";
|
||||||
|
// switch to normal mode
|
||||||
|
isEditing.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="paperbox-item card">
|
||||||
|
<div class="paperbox-item-multiline-words">
|
||||||
|
<div class="control" style="display: flex; flex-flow: row; align-items: center;">
|
||||||
|
<div v-show="isAdmin" class="icon is-small" style="margin-right: 1rem;">
|
||||||
|
<font-awesome-icon icon="fas fa-wrench"></font-awesome-icon>
|
||||||
|
</div>
|
||||||
|
<div>{{ username }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="isEditing" class="field">
|
||||||
|
<label class="label">{{ $t('userItem-newPassword') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input v-model="editingPassword" class="input" type="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="isEditing" class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input v-model="editingIsAdmin" type="checkbox"><span>{{ $t('userItem-isAdmin') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" v-show="!isEditing" @click="editItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon icon="fas fa-pen"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="paperbox-item-icon control" v-show="!isEditing" @click="deleteItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-trash"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" v-show="isEditing" @click="updateItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-check"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="paperbox-item-icon control" v-show="isEditing" @click="cancelUpdateItem">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-times"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import DateTimePicker, { TabType } from '@/components/DateTimePicker.vue';
|
|
||||||
import { resolveLoopRules4UI, getDayInMonth } from '@/utils/datetime';
|
|
||||||
import { WEEK_NAMES } from '@/utils/calendar-names';
|
|
||||||
import { format } from '@/utils/utils';
|
|
||||||
|
|
||||||
type LoopMethod = 'never' | 'day' | 'week' | 'month' | 'year';
|
|
||||||
type StopMethod = 'forever' | 'datetime' | 'times';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string;
|
|
||||||
startDate: Date;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const stopPicker = ref<InstanceType<typeof DateTimePicker>>();
|
|
||||||
|
|
||||||
const loopMethod = ref<LoopMethod>('never');
|
|
||||||
const daySpan = ref<number>(1);
|
|
||||||
const weekSpan = ref<number>(1);
|
|
||||||
const monthSpan = ref<number>(1);
|
|
||||||
const yearSpan = ref<number>(1);
|
|
||||||
// default: today's weekday checked (Monday-first 0-6)
|
|
||||||
const initialWeekday = (new Date().getDay() + 6) % 7;
|
|
||||||
const weekChecks = ref<boolean[]>(Array.from({ length: 7 }, (_, i) => i === initialWeekday));
|
|
||||||
const monthMode = ref<'A' | 'B' | 'C' | 'D'>('A');
|
|
||||||
const isStrict = ref<boolean>(true);
|
|
||||||
const stopMethod = ref<StopMethod>('forever');
|
|
||||||
const stopTimes = ref<number>(1);
|
|
||||||
const stopDateTime = ref<Date>(new Date());
|
|
||||||
|
|
||||||
// region: Serialize / parse
|
|
||||||
|
|
||||||
const buildString = (): string => {
|
|
||||||
let loopPart = '';
|
|
||||||
switch (loopMethod.value) {
|
|
||||||
case 'never':
|
|
||||||
return '';
|
|
||||||
case 'day':
|
|
||||||
loopPart = `D${daySpan.value}`;
|
|
||||||
break;
|
|
||||||
case 'week': {
|
|
||||||
const checks = weekChecks.value.map(b => b ? 'T' : 'F').join('');
|
|
||||||
loopPart = `W${checks}${weekSpan.value}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'month':
|
|
||||||
loopPart = `M${isStrict.value ? 'S' : 'R'}${monthMode.value}${monthSpan.value}`;
|
|
||||||
break;
|
|
||||||
case 'year':
|
|
||||||
loopPart = `Y${isStrict.value ? 'S' : 'R'}${yearSpan.value}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let stopPart = '';
|
|
||||||
switch (stopMethod.value) {
|
|
||||||
case 'forever':
|
|
||||||
stopPart = '-F';
|
|
||||||
break;
|
|
||||||
case 'datetime':
|
|
||||||
stopPart = `-D${Math.floor(stopDateTime.value.getTime() / 60000)}`;
|
|
||||||
break;
|
|
||||||
case 'times':
|
|
||||||
stopPart = `-T${stopTimes.value}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return loopPart + stopPart;
|
|
||||||
};
|
|
||||||
|
|
||||||
const serialized = computed<string>(() => buildString());
|
|
||||||
|
|
||||||
// Cast helpers: the parsed rule tuples are a union; index access is loose here.
|
|
||||||
const parseFromModel = (val: string): void => {
|
|
||||||
if (val === serialized.value) return;
|
|
||||||
if (val === '') {
|
|
||||||
loopMethod.value = 'never';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parsed = resolveLoopRules4UI(val);
|
|
||||||
if (typeof parsed === 'undefined') {
|
|
||||||
loopMethod.value = 'never';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loopRule = parsed[0];
|
|
||||||
const stopRule = parsed[1];
|
|
||||||
switch (loopRule[0]) {
|
|
||||||
case 0:
|
|
||||||
loopMethod.value = 'year';
|
|
||||||
isStrict.value = loopRule[1];
|
|
||||||
yearSpan.value = loopRule[2];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
loopMethod.value = 'month';
|
|
||||||
isStrict.value = loopRule[1];
|
|
||||||
monthMode.value = loopRule[2];
|
|
||||||
monthSpan.value = loopRule[3];
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
loopMethod.value = 'week';
|
|
||||||
weekChecks.value = [loopRule[1], loopRule[2], loopRule[3], loopRule[4], loopRule[5], loopRule[6], loopRule[7]];
|
|
||||||
weekSpan.value = loopRule[8];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
loopMethod.value = 'day';
|
|
||||||
daySpan.value = loopRule[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (stopRule[0]) {
|
|
||||||
case 0:
|
|
||||||
stopMethod.value = 'forever';
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
stopMethod.value = 'datetime';
|
|
||||||
// NOTE: treated as a plain UTC instant (day-precision stop), deviating
|
|
||||||
// from the legacy timezone-shifted display. See migration notes.
|
|
||||||
stopDateTime.value = new Date(stopRule[1] * 60000);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
stopMethod.value = 'times';
|
|
||||||
stopTimes.value = stopRule[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(() => props.modelValue, (val) => parseFromModel(val), { immediate: true });
|
|
||||||
watch(serialized, (val) => {
|
|
||||||
if (val !== props.modelValue) emit('update:modelValue', val);
|
|
||||||
});
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
const showLoopStop = computed(() => loopMethod.value !== 'never');
|
|
||||||
const showStrictMode = computed(() => loopMethod.value === 'month' || loopMethod.value === 'year');
|
|
||||||
|
|
||||||
const monthOptionTexts = computed(() => {
|
|
||||||
const d = getDayInMonth(
|
|
||||||
props.startDate.getFullYear(),
|
|
||||||
props.startDate.getMonth() + 1,
|
|
||||||
props.startDate.getDate()
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
A: format('Day {0} in month', d[0]),
|
|
||||||
B: format('Day {0} from the end of the month', d[1]),
|
|
||||||
C: format('Day {1} in week {0}', d[2], d[3] + 1),
|
|
||||||
D: format('Day {1} in week {0} from the end of the month', d[4], d[5] + 1),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const stopDateTimeText = computed(() => stopDateTime.value.toLocaleDateString());
|
|
||||||
|
|
||||||
const openStopPicker = () => {
|
|
||||||
stopPicker.value?.modal(stopDateTime.value, TabType.Day);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStopPickerConfirm = (date: Date) => {
|
|
||||||
stopDateTime.value = date;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="section">
|
|
||||||
<h2 class="subtitle">Event Loop</h2>
|
|
||||||
<div class="button-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="never" v-model="loopMethod">
|
|
||||||
<span>Never</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="day" v-model="loopMethod">
|
|
||||||
<span>Day</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="week" v-model="loopMethod">
|
|
||||||
<span>Week</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="month" v-model="loopMethod">
|
|
||||||
<span>Month</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="year" v-model="loopMethod">
|
|
||||||
<span>Year</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="loopMethod === 'day'">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Day span</label>
|
|
||||||
<div class="control">
|
|
||||||
<input v-model.number="daySpan" class="input spanpicker" type="number" min="1" max="100" step="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="loopMethod === 'week'">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Week span</label>
|
|
||||||
<div class="control">
|
|
||||||
<input v-model.number="weekSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Week options</label>
|
|
||||||
<div class="button-list">
|
|
||||||
<label v-for="(name, i) in WEEK_NAMES" :key="i" class="checkbox">
|
|
||||||
<input type="checkbox" v-model="weekChecks[i]">
|
|
||||||
<span>{{ name }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="loopMethod === 'month'">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Month span</label>
|
|
||||||
<div class="control">
|
|
||||||
<input v-model.number="monthSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Month mode</label>
|
|
||||||
<div class="button-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="A" v-model="monthMode">
|
|
||||||
<span>{{ monthOptionTexts.A }}</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="B" v-model="monthMode">
|
|
||||||
<span>{{ monthOptionTexts.B }}</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="C" v-model="monthMode">
|
|
||||||
<span>{{ monthOptionTexts.C }}</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="D" v-model="monthMode">
|
|
||||||
<span>{{ monthOptionTexts.D }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="loopMethod === 'year'">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Year span</label>
|
|
||||||
<div class="control">
|
|
||||||
<input v-model.number="yearSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="showStrictMode" class="section">
|
|
||||||
<h2 class="subtitle">Strict Mode in Event Loop</h2>
|
|
||||||
<p>You can choose strict mode or rough mode in following content. This is only effect on looped event.</p>
|
|
||||||
<div class="button-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" :value="true" v-model="isStrict">
|
|
||||||
<span>Strict Mode. If ordered day is not existing, skip it.</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" :value="false" v-model="isStrict">
|
|
||||||
<span>Rough mode. If ordered day is not existing, choose the day closing with original day to arrange event.</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="showLoopStop" class="section">
|
|
||||||
<h2 class="subtitle">Event Loop Stop</h2>
|
|
||||||
<div class="button-list">
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="forever" v-model="stopMethod">
|
|
||||||
<span>Forever</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="datetime" v-model="stopMethod">
|
|
||||||
<span>Date Time</span>
|
|
||||||
</label>
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" value="times" v-model="stopMethod">
|
|
||||||
<span>Times</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="stopMethod === 'datetime'">
|
|
||||||
<a class="button" @click="openStopPicker">
|
|
||||||
<span>{{ stopDateTimeText }}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div v-if="stopMethod === 'times'">
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input v-model.number="stopTimes" class="input spanpicker" type="number" min="1" max="100" step="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<DateTimePicker ref="stopPicker" @confirm="onStopPickerConfirm" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.section {
|
|
||||||
border-top: 1px solid rgba(219, 219, 219, .5);
|
|
||||||
padding-top: 1.25rem;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,67 +1,47 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { DisplayDay } from './types';
|
import type { CalendarCell } from './types';
|
||||||
import { format } from '@/utils/utils';
|
import CalendarItem from './CalendarItem.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps({
|
||||||
cells: DisplayDay[];
|
cells: {
|
||||||
weekNames: string[];
|
type: Array as () => CalendarCell[],
|
||||||
}>();
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
// split the flat 42-cell array into 6 rows of 7
|
||||||
(e: 'event-click', uuid: string): void
|
const rows = computed<CalendarCell[][]>(() => {
|
||||||
}>();
|
const r: CalendarCell[][] = []
|
||||||
|
for (let i = 0; i < 6; i++) r.push(props.cells.slice(i * 7, i * 7 + 7))
|
||||||
const rows = computed<DisplayDay[][]>(() => {
|
return r
|
||||||
const r: DisplayDay[][] = [];
|
})
|
||||||
for (let i = 0; i < 6; i++) r.push(props.cells.slice(i * 7, i * 7 + 7));
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
|
|
||||||
const overflowText = (count: number): string => {
|
|
||||||
return format('{0} items', count.toString());
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="calendar-grid">
|
<div class="calendarGrid card" style="padding: 1.25rem; display: flex; flex-flow: column;">
|
||||||
<div class="calendar-grid-header">
|
<div style="margin: 0 0 0.75em 0;">
|
||||||
<div v-for="(name, i) in weekNames" :key="i" :class="{ weekend: i >= 5 }"><b>{{ name }}</b></div>
|
<div v-for="i in 7" :key="i">
|
||||||
|
<b :style="i >= 6 ? { color: 'red' } : null">{{ $t('universal-week-' + i) }}</b>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(row, ri) in rows" :key="ri" class="calendar-grid-row">
|
|
||||||
<div v-for="(cell, ci) in row" :key="ci" class="calendar-grid-cell"
|
<div v-for="(row, ri) in rows" :key="ri">
|
||||||
:class="{ 'not-current-month': !cell.isCurrentMonth }">
|
<div
|
||||||
<p class="cell-title">
|
v-for="(cell, ci) in row"
|
||||||
<b>{{ cell.day }}</b>
|
:key="ci"
|
||||||
<span>{{ cell.subcalendar }}</span>
|
:isCurrentMonth="cell.isCurrentMonth ? 'true' : 'false'"
|
||||||
</p>
|
>
|
||||||
<div v-for="(e, ei) in cell.events.slice(0, 4)" :key="ei" class="event-bar"
|
<CalendarItem :cell="cell" />
|
||||||
:style="{ background: e.color }" @click="emit('event-click', e.uuid)"></div>
|
|
||||||
<p v-if="cell.events.length > 4" class="cell-overflow">{{ overflowText(cell.events.length) }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-grid {
|
.calendarGrid > div:nth-child(n + 2) > div {
|
||||||
display: flex;
|
border-top: 0 solid black;
|
||||||
flex-flow: column;
|
border-left: 0 solid black;
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-grid-header,
|
|
||||||
.calendar-grid-row {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-grid-cell {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
border-top: 1px solid black;
|
|
||||||
border-left: 1px solid black;
|
|
||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
|
|
||||||
@@ -74,37 +54,28 @@ const overflowText = (count: number): string => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-grid-cell.not-current-month {
|
.calendarGrid > div:nth-child(n + 2) > div:nth-child(1) {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarGrid > div:nth-child(2) > div {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarGrid > div > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarGrid > div > div[isCurrentMonth=false] {
|
||||||
background: #d0d0d0;
|
background: #d0d0d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* remove the double border between adjacent cells */
|
.calendarGrid > div {
|
||||||
.calendar-grid-row .calendar-grid-cell:nth-child(n+2) {
|
display: flex;
|
||||||
border-left: 0;
|
flex-flow: row;
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-grid-row:nth-child(n+2) .calendar-grid-cell {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-title {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-overflow {
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-bar {
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
height: 0.75rem;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekend {
|
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { CalendarCell } from './types';
|
||||||
|
import { i18n } from '@/utils/i18n';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
cell: {
|
||||||
|
type: Object as () => CalendarCell,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface EventBoxSlot {
|
||||||
|
filled: boolean
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// The grid counts ALL events in the cell (visibility is ignored here, matching
|
||||||
|
// legacy behaviour). Render the (up to) 4 coloured event-box slots.
|
||||||
|
const slots = computed<EventBoxSlot[]>(() => {
|
||||||
|
const events = props.cell.events
|
||||||
|
const arr: EventBoxSlot[] = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (i < events.length) {
|
||||||
|
arr.push({ filled: true, color: events[i]!.color })
|
||||||
|
} else {
|
||||||
|
arr.push({ filled: false, color: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
|
||||||
|
const moreLabel = computed<string | null>(() => {
|
||||||
|
const len = props.cell.events.length
|
||||||
|
return len > 4 ? i18n.global.t('calendar-calendar-stripedEvents', [len]) : null
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p><b>{{ cell.day }}</b><span></span></p>
|
||||||
|
<div
|
||||||
|
v-for="(slot, i) in slots"
|
||||||
|
:key="i"
|
||||||
|
class="calendarItem-eventBox"
|
||||||
|
:enableDisplay="slot.filled ? 'true' : 'false'"
|
||||||
|
:style="slot.filled ? { background: slot.color } : null"
|
||||||
|
></div>
|
||||||
|
<p v-if="moreLabel !== null">{{ moreLabel }}</p>
|
||||||
|
<p v-else> </p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div.calendarItem-eventBox {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.calendarItem-eventBox[enableDisplay=true] {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.calendarItem-eventBox[enableDisplay=false] {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
name: string;
|
|
||||||
subtitle?: string;
|
|
||||||
isShow: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'toggle'): void
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="paperbox-item card">
|
|
||||||
<div class="paperbox-item-words">
|
|
||||||
<p v-if="subtitle === undefined"><b>{{ name }}</b></p>
|
|
||||||
<div v-else>
|
|
||||||
<b>{{ name }}</b>
|
|
||||||
<p>
|
|
||||||
<span>Shared by: </span>
|
|
||||||
<span>{{ subtitle }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="paperbox-item-icon control" @click="emit('toggle')">
|
|
||||||
<a class="button">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<font-awesome-icon :icon="isShow ? 'fas fa-eye' : 'fas fa-eye-slash'"></font-awesome-icon>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'toggle'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
emit('toggle');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="paperbox-item card">
|
||||||
|
<div class="paperbox-item-words">
|
||||||
|
<p>{{ name }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" v-show="isVisible" @click="toggle">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon icon="fas fa-eye"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="paperbox-item-icon control" v-show="!isVisible" @click="toggle">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-eye-slash"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ScheduleEventItem } from './types';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object as () => ScheduleEventItem,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'edit'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit('edit');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schedule-event-outter card" @click="onClick">
|
||||||
|
<div class="schedule-event-color" :style="{ background: event.color }"> </div>
|
||||||
|
<div class="schedule-event-inner">
|
||||||
|
<div class="schedule-event-words">
|
||||||
|
<p class="level-item"><b>{{ event.title }}</b></p>
|
||||||
|
<p class="level-item">{{ event.description }}</p>
|
||||||
|
<p class="level-item"><span>{{ event.start }}</span>-<span>{{ event.end }}</span></p>
|
||||||
|
<p v-if="event.loopText !== ''">
|
||||||
|
<span class="icon is-small"><font-awesome-icon icon="fas fa-retweet"></font-awesome-icon></span>
|
||||||
|
<span>{{ event.loopText }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="schedule-event-icon">
|
||||||
|
<span v-if="event.isLocked" class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-lock"></font-awesome-icon></span>
|
||||||
|
<span v-if="event.timezoneWarning" class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-globe"></font-awesome-icon></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.schedule-event-outter {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-event-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-event-words {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-event-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-event-color {
|
||||||
|
width: 0.75rem;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,48 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DisplayDay } from './types';
|
import { universalGetMonth, universalGetDayOfWeek } from '@/utils/i18n';
|
||||||
import { MONTH_NAMES, WEEK_NAMES } from '@/utils/calendar-names';
|
import type { CalendarCell } from './types';
|
||||||
|
import ScheduleItem from './ScheduleItem.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps({
|
||||||
days: DisplayDay[];
|
cells: {
|
||||||
}>();
|
type: Array as () => CalendarCell[],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'event-click', uuid: string): void
|
(e: 'edit', uuid: string): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
|
const onEdit = (uuid: string) => {
|
||||||
|
emit('edit', uuid);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="schedule-list">
|
<div>
|
||||||
<div v-for="(day, di) in days" :key="di" class="schedule-day container">
|
<div v-for="(cell, i) in cells" :key="i" class="schedule-day container">
|
||||||
<div class="schedule-day-words">
|
<div class="schedule-day-words">
|
||||||
<b>{{ MONTH_NAMES[day.month - 1] }}</b>
|
<b>{{ universalGetMonth(cell.month - 1) }}</b> <b>{{ cell.day }}</b> <b>{{
|
||||||
<b>{{ day.day }}</b>
|
universalGetDayOfWeek(cell.dayOfWeek - 1) }}</b>
|
||||||
<b>{{ WEEK_NAMES[day.dayOfWeek - 1] }}</b>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="schedule-event-list">
|
<div class="schedule-event-list">
|
||||||
<template v-for="(ev, ei) in day.events" :key="ei">
|
<template v-for="(ev, j) in cell.events" :key="j">
|
||||||
<div v-if="ev.isVisible" class="schedule-event-outter card" @click="emit('event-click', ev.uuid)">
|
<ScheduleItem v-if="ev.isVisible" :event="ev" @edit="onEdit(ev.uuid)" />
|
||||||
<div class="schedule-event-color" :style="{ background: ev.color }"></div>
|
|
||||||
<div class="schedule-event-inner">
|
|
||||||
<div class="schedule-event-words">
|
|
||||||
<p class="level-item"><b>{{ ev.title }}</b></p>
|
|
||||||
<p class="level-item">{{ ev.description }}</p>
|
|
||||||
<p class="level-item"><span>{{ ev.start }}</span>-<span>{{ ev.end }}</span></p>
|
|
||||||
<p v-if="ev.loopText !== ''">
|
|
||||||
<span class="icon is-small"><font-awesome-icon icon="fas fa-retweet"></font-awesome-icon></span>
|
|
||||||
<span>{{ ev.loopText }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="schedule-event-icon">
|
|
||||||
<span v-if="ev.isLocked" class="icon is-small">
|
|
||||||
<font-awesome-icon icon="fas fa-lock"></font-awesome-icon>
|
|
||||||
</span>
|
|
||||||
<span v-if="ev.timezoneWarning" class="icon is-small">
|
|
||||||
<font-awesome-icon icon="fas fa-globe"></font-awesome-icon>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,53 +46,12 @@ const emit = defineEmits<{
|
|||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-day-words b {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-event-list {
|
.schedule-event-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-event-outter {
|
.schedule-day:nth-child(n + 2) {
|
||||||
display: flex;
|
border-top: 1px solid rgba(219, 219, 219, 0.5);
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-event-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-event-words {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-event-icon {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-event-color {
|
|
||||||
width: 0.75rem;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule-list .schedule-day:nth-child(n+2) {
|
|
||||||
border-top: 1px solid rgba(219, 219, 219, .5);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'toggle'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
emit('toggle');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="paperbox-item card">
|
||||||
|
<div class="paperbox-item-words">
|
||||||
|
<b>{{ name }}</b>
|
||||||
|
<p>
|
||||||
|
<span>{{ $t('sharedItem-sharedBy') }}</span>
|
||||||
|
<span>{{ username }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" v-show="isVisible" @click="toggle">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon icon="fas fa-eye"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="paperbox-item-icon control" v-show="!isVisible" @click="toggle">
|
||||||
|
<a class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-eye-slash"></font-awesome-icon></span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,26 +1,25 @@
|
|||||||
/** A single analyzed event occurrence placed into a calendar day cell. */
|
/** A single expanded event occurrence placed into a calendar cell / schedule row. */
|
||||||
export interface DisplayEvent {
|
export interface ScheduleEventItem {
|
||||||
uuid: string;
|
uuid: string
|
||||||
belongTo: string;
|
belongTo: string
|
||||||
title: string;
|
title: string
|
||||||
description: string;
|
description: string
|
||||||
color: string;
|
color: string
|
||||||
isVisible: boolean;
|
isVisible: boolean
|
||||||
isLocked: boolean;
|
isLocked: boolean
|
||||||
loopText: string;
|
loopText: string
|
||||||
timezoneWarning: boolean;
|
timezoneWarning: boolean
|
||||||
start: string;
|
start: string
|
||||||
end: string;
|
end: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A single day cell in the 6x7 calendar grid, carrying its events. */
|
/** One cell of the 6x7 month grid (also one day group in the schedule list). */
|
||||||
export interface DisplayDay {
|
export interface CalendarCell {
|
||||||
/** 1-12 */
|
/** 1-based month. */
|
||||||
month: number;
|
month: number
|
||||||
day: number;
|
day: number
|
||||||
/** 1-7, Monday = 1 */
|
/** 1-based day of week, Monday = 1 ... Sunday = 7. */
|
||||||
dayOfWeek: number;
|
dayOfWeek: number
|
||||||
isCurrentMonth: boolean;
|
isCurrentMonth: boolean
|
||||||
subcalendar: string;
|
events: ScheduleEventItem[]
|
||||||
events: DisplayEvent[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const cancelUpdateItem = () => {
|
|||||||
<div class="paperbox-item-words">
|
<div class="paperbox-item-words">
|
||||||
<p v-show="!isEditing">{{ name }}</p>
|
<p v-show="!isEditing">{{ name }}</p>
|
||||||
<div v-show="isEditing" class="control">
|
<div v-show="isEditing" class="control">
|
||||||
<input v-model="editingName" class="input" type="text"></input>
|
<input v-model="editingName" class="input" type="text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { lineBreaker2Br } from '@/utils/utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
uuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'delete', uuid: string): void
|
||||||
|
(e: 'update', uuid: string, data: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isEditing = ref<boolean>(false);
|
||||||
|
const editingData = ref<string>("");
|
||||||
|
|
||||||
|
const editItem = () => {
|
||||||
|
// copy current data to textarea
|
||||||
|
editingData.value = props.data;
|
||||||
|
// switch to edit mode
|
||||||
|
isEditing.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = () => {
|
||||||
|
emit('delete', props.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItem = () => {
|
||||||
|
const new_data = editingData.value;
|
||||||
|
// clean data
|
||||||
|
editingData.value = "";
|
||||||
|
// switch to normal mode
|
||||||
|
isEditing.value = false;
|
||||||
|
emit('update', props.uuid, new_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelUpdateItem = () => {
|
||||||
|
// clean data
|
||||||
|
editingData.value = "";
|
||||||
|
// switch to normal mode
|
||||||
|
isEditing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="paperbox-item card">
|
||||||
|
<div class="paperbox-item-words">
|
||||||
|
<p v-show="!isEditing" v-html="lineBreaker2Br(data)"></p>
|
||||||
|
<textarea v-show="isEditing" v-model="editingData" class="textarea"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" v-show="!isEditing" @click="editItem">
|
||||||
|
<button class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-pen"></font-awesome-icon></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="paperbox-item-icon control" v-show="!isEditing" @click="deleteItem">
|
||||||
|
<button class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-trash"></font-awesome-icon></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paperbox-item-icon control" v-show="isEditing" @click="updateItem">
|
||||||
|
<button class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-check"></font-awesome-icon></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="paperbox-item-icon control" v-show="isEditing" @click="cancelUpdateItem">
|
||||||
|
<button class="button"><span class="icon is-small"><font-awesome-icon
|
||||||
|
icon="fas fa-times"></font-awesome-icon></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
export const messages: Record<string, string> = {
|
||||||
|
// page names (reserved for future document.title use)
|
||||||
|
'pageName-home': 'coconut-leaf - A light, self-host calendar system.',
|
||||||
|
'pageName-collection': 'coconut-leaf - Collection',
|
||||||
|
'pageName-calendar': 'coconut-leaf - Calendar',
|
||||||
|
'pageName-event': 'coconut-leaf - Event',
|
||||||
|
'pageName-todo': 'coconut-leaf - Todo',
|
||||||
|
'pageName-admin': 'coconut-leaf - Admin',
|
||||||
|
'pageName-login': 'coconut-leaf - Login',
|
||||||
|
|
||||||
|
// header navbar
|
||||||
|
'header-nav-home': 'Home',
|
||||||
|
'header-nav-collection': 'Collection',
|
||||||
|
'header-nav-calendar': 'Calendar',
|
||||||
|
'header-nav-todo': 'Todo',
|
||||||
|
'header-nav-admin': 'Admin',
|
||||||
|
'header-user-login': 'Login',
|
||||||
|
'header-user-logout': 'Logout',
|
||||||
|
'header-language': 'Language',
|
||||||
|
|
||||||
|
// universal datetime labels
|
||||||
|
'universal-text-year': 'Year',
|
||||||
|
'universal-text-month': 'Month',
|
||||||
|
'universal-text-day': 'Day',
|
||||||
|
'universal-text-hour': 'Hour',
|
||||||
|
'universal-text-minute': 'Minute',
|
||||||
|
'universal-week-1': 'Monday',
|
||||||
|
'universal-week-2': 'Tuesday',
|
||||||
|
'universal-week-3': 'Wednesday',
|
||||||
|
'universal-week-4': 'Thursday',
|
||||||
|
'universal-week-5': 'Friday',
|
||||||
|
'universal-week-6': 'Saturday',
|
||||||
|
'universal-week-7': 'Sunday',
|
||||||
|
'universal-month-1': 'January',
|
||||||
|
'universal-month-2': 'February',
|
||||||
|
'universal-month-3': 'March',
|
||||||
|
'universal-month-4': 'April',
|
||||||
|
'universal-month-5': 'May',
|
||||||
|
'universal-month-6': 'June',
|
||||||
|
'universal-month-7': 'July',
|
||||||
|
'universal-month-8': 'August',
|
||||||
|
'universal-month-9': 'September',
|
||||||
|
'universal-month-10': 'October',
|
||||||
|
'universal-month-11': 'November',
|
||||||
|
'universal-month-12': 'December',
|
||||||
|
|
||||||
|
// messagebox
|
||||||
|
'messagebox-confirm': 'OK',
|
||||||
|
'messagebox-title': 'Notification',
|
||||||
|
|
||||||
|
// datetimepicker
|
||||||
|
'datetimepicker-confirm': 'OK',
|
||||||
|
'datetimepicker-cancel': 'Cancel',
|
||||||
|
|
||||||
|
// js operation messages
|
||||||
|
'js-fail-login': 'Fail to login. Please check your username or password.',
|
||||||
|
'js-fail-logout': 'Fail to logout due to unknow reason. Consider refreshing page to solve problem.',
|
||||||
|
'js-fail-get': 'A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
|
||||||
|
'js-fail-add': 'An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
|
||||||
|
'js-fail-update': 'An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
|
||||||
|
'js-fail-delete': 'A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.',
|
||||||
|
'js-success': 'Operation OK.',
|
||||||
|
'js-fail-form': 'Your filled event form is not fufilled or have error. Please check it and try again.',
|
||||||
|
|
||||||
|
// home page
|
||||||
|
'home-tagline': 'A light, self-host and multi-account calendar system.',
|
||||||
|
'home-intent': 'The original intention of this system is served for yyc12345 personal use.',
|
||||||
|
'home-source': 'See our {link} for the source code in detail.',
|
||||||
|
'home-source-link': 'GitHub project',
|
||||||
|
'home-license': 'The source code of this project is licensed under {link}.',
|
||||||
|
'home-license-link': 'AGPL v3',
|
||||||
|
|
||||||
|
// login
|
||||||
|
'login-form-username': 'Username',
|
||||||
|
'login-form-password': 'Password',
|
||||||
|
'login-form-login': 'Login',
|
||||||
|
|
||||||
|
// todo
|
||||||
|
'todo-todoList': 'Todo list',
|
||||||
|
|
||||||
|
// calendar page
|
||||||
|
'calendar-calendar-today': 'Today',
|
||||||
|
'calendar-calendar-add': 'Add...',
|
||||||
|
'calendar-calendar-stripedEvents': '{0} items',
|
||||||
|
'calendar-calendar-scheduleList': 'Schedule',
|
||||||
|
'calendar-tabcontrol-tabCalendar': 'Calendar',
|
||||||
|
'calendar-tabcontrol-tabCollection': 'Collection',
|
||||||
|
'calendar-tabcontrol-tabDisplay': 'Display',
|
||||||
|
'calendar-owned-list': 'My collections',
|
||||||
|
'calendar-shared-list': 'Shared collections',
|
||||||
|
'calendar-display-firstDayOfWeek': 'The first day of week',
|
||||||
|
'calendar-display-subcalendar': 'Sub-Calendar',
|
||||||
|
'calendar-display-subcalendar-chineseLunisolarCalendar': 'Chinese Lunisolar Calendar',
|
||||||
|
'calendar-display-subcalendar-none': 'None',
|
||||||
|
|
||||||
|
// collection page
|
||||||
|
'collection-owned-list': 'Owned',
|
||||||
|
'collection-sharing-list': 'Sharing target',
|
||||||
|
'collection-sharing-editing': 'Editing: ',
|
||||||
|
|
||||||
|
// event page
|
||||||
|
'event-header': 'Edit Event',
|
||||||
|
'event-title': 'Title',
|
||||||
|
'event-description': 'Description',
|
||||||
|
'event-color': 'Color',
|
||||||
|
'event-collection': 'Collection',
|
||||||
|
'event-startDateTime': 'Start Date Time',
|
||||||
|
'event-endDateTime': 'Stop Date Time',
|
||||||
|
'event-btnSpot': 'Spot',
|
||||||
|
'event-btnFullDay': 'Full day',
|
||||||
|
'event-loop': 'Event Loop',
|
||||||
|
'event-loop-never': 'Never',
|
||||||
|
'event-loop-day': 'Day',
|
||||||
|
'event-loop-week': 'Week',
|
||||||
|
'event-loop-month': 'Month',
|
||||||
|
'event-loop-year': 'Year',
|
||||||
|
'event-loopDay-span': 'Day span',
|
||||||
|
'event-loopWeek-span': 'Week span',
|
||||||
|
'event-loopWeek-option': 'Week options',
|
||||||
|
'event-loopMonth-span': 'Month span',
|
||||||
|
'event-loopMonth-option': 'Month mode',
|
||||||
|
'event-loopWeek-optionA': 'Day {0} in month',
|
||||||
|
'event-loopWeek-optionB': 'Day {0} from the end of the month',
|
||||||
|
'event-loopWeek-optionC': 'Day {1} in week {0}',
|
||||||
|
'event-loopWeek-optionD': 'Day {1} in week {0} from the end of the month',
|
||||||
|
'event-loopYear-span': 'Year span',
|
||||||
|
'event-loopStop': 'Event Loop Stop',
|
||||||
|
'event-loopStop-forever': 'Forever',
|
||||||
|
'event-loopStop-datetime': 'Date Time',
|
||||||
|
'event-loopStop-times': 'Times',
|
||||||
|
'event-timezone-title': 'Timezone',
|
||||||
|
'event-timezone-warning': 'The timezone of this event is not corresponding with your current timezone. All of date and time in this page are shown as the original timezone of this event. You can choose a timezone option in follwing content. If you are not familar with this, please pick keep timezone.',
|
||||||
|
'event-timezone-keep': 'Keep timezone',
|
||||||
|
'event-timezone-replace': 'Use my timezone',
|
||||||
|
'event-strictMode-title': 'Strict Mode in Event Loop',
|
||||||
|
'event-strictMode-warning': 'You can choose strict mode or rough mode in following content. This is only effect on looped event.',
|
||||||
|
'event-strictMode-strict': 'Strict Mode. If ordered day is not existing, skip it.',
|
||||||
|
'event-strictMode-rough': 'Rough mode. If ordered day is not existing, choose the day closing with original day to arrange event.',
|
||||||
|
'event-btnSubmit': 'Submit',
|
||||||
|
'event-btnCancel': 'Cancel',
|
||||||
|
|
||||||
|
// shared item
|
||||||
|
'sharedItem-sharedBy': 'Shared by: ',
|
||||||
|
|
||||||
|
// admin page
|
||||||
|
'admin-tabcontrol-tabProfile': 'My Profile',
|
||||||
|
'admin-tabcontrol-tabToken': 'Manage Multi-login',
|
||||||
|
'admin-tabcontrol-tabUserList': 'Manager User',
|
||||||
|
'admin-changePassword': 'Change Password',
|
||||||
|
'admin-manageToken': 'Manage multi-login',
|
||||||
|
'admin-manageToken-desc': 'Manage the multi-login of the current account. You can forced logout some login in there.',
|
||||||
|
'admin-userList': 'User List',
|
||||||
|
|
||||||
|
// admin user/token items
|
||||||
|
'userItem-newPassword': 'New Password',
|
||||||
|
'userItem-isAdmin': 'Is Admin',
|
||||||
|
'tokenItem-ua': 'User Agent: ',
|
||||||
|
'tokenItem-ip': 'IP: ',
|
||||||
|
'tokenItem-expireOn': 'Expire On: ',
|
||||||
|
'tokenItem-isMe': 'This is the login credentials you are currently using.',
|
||||||
|
|
||||||
|
// datetime loop rule human-readable text (English translations authored for
|
||||||
|
// this migration — the legacy en-US properties omitted these keys.)
|
||||||
|
'datetime-loopStopRuleText-infinity': 'Loop forever.',
|
||||||
|
'datetime-loopStopRuleText-datetime': 'Stop looping at {0}.',
|
||||||
|
'datetime-loopStopRuleText-times': 'Loop {0} time(s).',
|
||||||
|
'datetime-loopRuleText-modeStrict': 'Strict mode.',
|
||||||
|
'datetime-loopRuleText-modeRough': 'Rough mode.',
|
||||||
|
'datetime-loopRuleText-year': 'Loop once every {0} year(s) on {1}.',
|
||||||
|
'datetime-loopRuleText-monthA': 'Loop once on day {1} of every {0} month(s).',
|
||||||
|
'datetime-loopRuleText-monthB': 'Loop once on the {1}th day from the end of every {0} month(s).',
|
||||||
|
'datetime-loopRuleText-monthC': 'Loop once every {0} month(s) on the {2} of week {1}.',
|
||||||
|
'datetime-loopRuleText-monthD': 'Loop once every {0} month(s) on the {2} of week {1} from the end.',
|
||||||
|
'datetime-loopRuleText-week': 'Loop once every {0} week(s) on {1}.',
|
||||||
|
'datetime-loopRuleText-day': 'Loop once every {0} day(s).',
|
||||||
|
|
||||||
|
// not found page
|
||||||
|
'notfound-title': 'Oops!',
|
||||||
|
'notfound-desc1': 'You are wandering in the desert of coconut-leaf.',
|
||||||
|
'notfound-desc2': 'Please back to previous page and try again.',
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user