first commit
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
create table card_set
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
name TEXT not null,
|
||||||
|
forward TEXT not null,
|
||||||
|
backward TEXT not null,
|
||||||
|
filter TEXT not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table settings
|
||||||
|
(
|
||||||
|
id text
|
||||||
|
primary key,
|
||||||
|
value text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table word_group
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
name TEXT not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table words
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
key TEXT not null,
|
||||||
|
value TEXT not null,
|
||||||
|
tags TEXT not null,
|
||||||
|
more TEXT,
|
||||||
|
group_id integer default 1 not null
|
||||||
|
constraint words_word_group_id_fk
|
||||||
|
references word_group
|
||||||
|
on update cascade on delete cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create table card_stats
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
word_id INTEGER not null
|
||||||
|
references words
|
||||||
|
on delete cascade,
|
||||||
|
set_id TEXT not null
|
||||||
|
references card_set
|
||||||
|
on delete cascade,
|
||||||
|
score INTEGER default 1 not null,
|
||||||
|
last_opened integer not null
|
||||||
|
);
|
||||||
|
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" translate="no">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#7c3aed" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<title>JapLearn</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header class="app-header">
|
||||||
|
<h1>JapLearn</h1>
|
||||||
|
<p>Интервальные повторения японских слов</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="page-container" class="app-content">
|
||||||
|
<!-- Страницы рендерятся сюда динамически -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav id="app-nav" class="app-nav">
|
||||||
|
<button class="nav-btn active" data-page="words">📚 Слова</button>
|
||||||
|
<button class="nav-btn" data-page="sets">🗂️ Колоды</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
use crate::data_provider::card_stats::{
|
||||||
|
add_stat, delete_stat, load_stats_of_set, update_stat_score,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use rand::distr::weighted::WeightedIndex;
|
||||||
|
use rand::distr::Distribution;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
|
use rand::rng;
|
||||||
|
use rand::rngs::ThreadRng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
const MAX_HISTORY_LEN: usize = 20;
|
||||||
|
const MAX_HISTORY_LEN_PART: f32 = 0.33;
|
||||||
|
const MAX_SCORE: i32 = 25;
|
||||||
|
const FADE_PER_DAY: f32 = 0.95;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct WordData {
|
||||||
|
pub id: u32,
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
pub tags: String,
|
||||||
|
pub additional: HashMap<String, String>,
|
||||||
|
pub group_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WordData {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: 0,
|
||||||
|
key: String::new(),
|
||||||
|
value: String::new(),
|
||||||
|
tags: String::new(),
|
||||||
|
additional: Default::default(),
|
||||||
|
group_id: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WordGroup {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct CardStatistics {
|
||||||
|
pub id: u32,
|
||||||
|
pub word_id: u32,
|
||||||
|
pub set_id: u32,
|
||||||
|
pub last_open: DateTime<Utc>,
|
||||||
|
pub score: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardStatistics {
|
||||||
|
pub fn update(&mut self, status: WordOpenMode) {
|
||||||
|
match status {
|
||||||
|
WordOpenMode::Easy => {
|
||||||
|
self.score = (self.calculated_score() + 5.0).round() as i32;
|
||||||
|
}
|
||||||
|
WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as i32,
|
||||||
|
WordOpenMode::Hard => {
|
||||||
|
self.score = (self.calculated_score() - 1.0).round() as i32;
|
||||||
|
}
|
||||||
|
WordOpenMode::None => {
|
||||||
|
self.score = (self.calculated_score() * 0.5) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.score < 1 {
|
||||||
|
self.score = 1
|
||||||
|
} else if self.score > MAX_SCORE {
|
||||||
|
self.score = MAX_SCORE
|
||||||
|
}
|
||||||
|
self.last_open = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculated_score(&self) -> f32 {
|
||||||
|
let time = Utc::now() - self.last_open;
|
||||||
|
let days = time.num_days();
|
||||||
|
let multiplier = FADE_PER_DAY.powi(days as i32);
|
||||||
|
self.score as f32 * multiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum WordOpenMode {
|
||||||
|
Easy,
|
||||||
|
Ok,
|
||||||
|
Hard,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CardSet {
|
||||||
|
words: Vec<WordData>,
|
||||||
|
set: Vec<CardStatistics>,
|
||||||
|
current_word_index: Option<usize>,
|
||||||
|
state: Arc<Mutex<AppState>>,
|
||||||
|
order_module: OrderModule,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CardSetSettings {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub forward: String,
|
||||||
|
pub backward: String,
|
||||||
|
pub filter: String,
|
||||||
|
pub count: Option<usize>,
|
||||||
|
pub worst_words_list: Option<Vec<WordData>>,
|
||||||
|
pub open_mode: SetOrderMode
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardSet {
|
||||||
|
pub fn new(settings: &CardSetSettings, state: Arc<Mutex<AppState>>) -> Self {
|
||||||
|
let state_for = state.clone();
|
||||||
|
let state_locked = state.lock().unwrap();
|
||||||
|
|
||||||
|
let mut current_set = load_stats_of_set(&settings, &state_locked.connection);
|
||||||
|
let last_list = settings.get_word_list(&state_locked);
|
||||||
|
let saved_ids = current_set.iter().map(|l| l.word_id).collect::<Vec<u32>>();
|
||||||
|
let word_ids = last_list.iter().map(|l| l.id).collect::<Vec<u32>>();
|
||||||
|
|
||||||
|
last_list
|
||||||
|
.iter()
|
||||||
|
.filter(|word| !saved_ids.contains(&word.id))
|
||||||
|
.map(|word| CardStatistics {
|
||||||
|
id: 0,
|
||||||
|
word_id: word.id.clone(),
|
||||||
|
last_open: Utc::now(),
|
||||||
|
score: 1,
|
||||||
|
set_id: settings.id.clone(),
|
||||||
|
})
|
||||||
|
.for_each(|mut new_statistic| {
|
||||||
|
add_stat(&mut new_statistic, &state_locked.connection);
|
||||||
|
current_set.push(new_statistic);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
for stat in current_set.clone() {
|
||||||
|
if !word_ids.contains(&stat.word_id) {
|
||||||
|
delete_stat(&stat, &state_locked.connection);
|
||||||
|
current_set.remove(index);
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
set: current_set,
|
||||||
|
words: last_list,
|
||||||
|
current_word_index: None,
|
||||||
|
state: state_for,
|
||||||
|
order_module: match settings.open_mode {
|
||||||
|
SetOrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()),
|
||||||
|
SetOrderMode::TrainWorstFirst => {
|
||||||
|
OrderModule::WorstWordsSRS(WorstWordsSRSModule::new())
|
||||||
|
}
|
||||||
|
SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) -> (WordData, CardStatistics) {
|
||||||
|
let index = match self.order_module.clone() {
|
||||||
|
OrderModule::SemiRandomSRS(mut module) => {
|
||||||
|
if module.initializated == false {
|
||||||
|
module.init(self)
|
||||||
|
}
|
||||||
|
let index = module.next(self);
|
||||||
|
self.order_module = OrderModule::SemiRandomSRS(module);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
OrderModule::RandomSRS(mut module) => {
|
||||||
|
if module.initializated == false {
|
||||||
|
module.init(self)
|
||||||
|
}
|
||||||
|
let index = module.next(self);
|
||||||
|
self.order_module = OrderModule::RandomSRS(module);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
OrderModule::WorstWordsSRS(mut module) => {
|
||||||
|
if module.initializated == false {
|
||||||
|
module.init(self)
|
||||||
|
}
|
||||||
|
let index = module.next(self);
|
||||||
|
self.order_module = OrderModule::WorstWordsSRS(module);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.current_word_index = Some(index);
|
||||||
|
(self.words[index].clone(), self.set[index].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(&mut self, status: WordOpenMode) {
|
||||||
|
if let None = self.current_word_index {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let index = self.current_word_index.unwrap();
|
||||||
|
|
||||||
|
let word = &mut self.set[index];
|
||||||
|
word.update(status);
|
||||||
|
|
||||||
|
match self.order_module.clone() {
|
||||||
|
OrderModule::SemiRandomSRS(mut module) => {
|
||||||
|
module.open(status, index, word.clone());
|
||||||
|
self.order_module = OrderModule::SemiRandomSRS(module);
|
||||||
|
}
|
||||||
|
OrderModule::RandomSRS(mut module) => {
|
||||||
|
module.open(status, index, word.clone());
|
||||||
|
self.order_module = OrderModule::RandomSRS(module);
|
||||||
|
}
|
||||||
|
OrderModule::WorstWordsSRS(mut module) => {
|
||||||
|
module.open(status, index, word.clone());
|
||||||
|
self.order_module = OrderModule::WorstWordsSRS(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_stat_score(word, &self.state.lock().unwrap().connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.set.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Copy, Eq)]
|
||||||
|
pub(crate) enum SetOrderMode {
|
||||||
|
Default,
|
||||||
|
TrainWorstFirst,
|
||||||
|
FullRandom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum OrderModule {
|
||||||
|
SemiRandomSRS(SemiRandomSRSModule),
|
||||||
|
RandomSRS(RandomSRSModule),
|
||||||
|
WorstWordsSRS(WorstWordsSRSModule),
|
||||||
|
}
|
||||||
|
|
||||||
|
trait SRSModule {
|
||||||
|
fn next(&mut self, set: &mut CardSet) -> usize;
|
||||||
|
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics);
|
||||||
|
fn init(&mut self, set: &mut CardSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RandomSRSModule {
|
||||||
|
backet: Vec<usize>,
|
||||||
|
initializated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RandomSRSModule {
|
||||||
|
fn new() -> RandomSRSModule {
|
||||||
|
Self{
|
||||||
|
backet: vec![],
|
||||||
|
initializated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SRSModule for RandomSRSModule {
|
||||||
|
fn next(&mut self, set: &mut CardSet) -> usize {
|
||||||
|
if self.backet.is_empty() {
|
||||||
|
self.backet = (0..set.words.len()).collect::<Vec<usize>>();
|
||||||
|
self.backet.shuffle(&mut rand::rng())
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backet.pop().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {}
|
||||||
|
|
||||||
|
fn init(&mut self, set: &mut CardSet) {
|
||||||
|
self.initializated = true;
|
||||||
|
self.backet = (0..set.words.len()).collect::<Vec<usize>>();
|
||||||
|
self.backet.shuffle(&mut rand::rng())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SemiRandomSRSModule {
|
||||||
|
history: Vec<usize>,
|
||||||
|
last_weights: WeightedIndex<f32>,
|
||||||
|
generator: ThreadRng,
|
||||||
|
initializated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SemiRandomSRSModule {
|
||||||
|
fn new() -> SemiRandomSRSModule {
|
||||||
|
SemiRandomSRSModule {
|
||||||
|
history: vec![],
|
||||||
|
last_weights: WeightedIndex::new([1.0]).unwrap(),
|
||||||
|
generator: rng(),
|
||||||
|
initializated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SRSModule for SemiRandomSRSModule {
|
||||||
|
fn next(&mut self, set: &mut CardSet) -> usize {
|
||||||
|
let index = self.last_weights.sample(&mut self.generator);
|
||||||
|
|
||||||
|
if self.history.contains(&index) {
|
||||||
|
return self.next(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.history.len() == self.history_len(set) {
|
||||||
|
self.history.remove(0);
|
||||||
|
}
|
||||||
|
self.history.push(index);
|
||||||
|
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, status: WordOpenMode, index: usize, word: CardStatistics) {
|
||||||
|
let new_weight = (100.0 / word.calculated_score()).powf(2.0);
|
||||||
|
self.last_weights
|
||||||
|
.update_weights(&[(index, &new_weight)])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&mut self, set: &mut CardSet) {
|
||||||
|
self.initializated = true;
|
||||||
|
let weights = set
|
||||||
|
.set
|
||||||
|
.iter()
|
||||||
|
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
|
||||||
|
.collect::<Vec<f32>>();
|
||||||
|
self.last_weights = WeightedIndex::new(weights).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SemiRandomSRSModule {
|
||||||
|
fn history_len(&self, set: &CardSet) -> usize {
|
||||||
|
min(
|
||||||
|
MAX_HISTORY_LEN,
|
||||||
|
(set.len() as f32 * MAX_HISTORY_LEN_PART) as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct WorstWordsSRSModule {
|
||||||
|
initializated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SRSModule for WorstWordsSRSModule {
|
||||||
|
fn next(&mut self, set: &mut CardSet) -> usize {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&mut self, set: &mut CardSet) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorstWordsSRSModule {
|
||||||
|
fn new() -> WorstWordsSRSModule {
|
||||||
|
WorstWordsSRSModule {
|
||||||
|
initializated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+902
@@ -0,0 +1,902 @@
|
|||||||
|
{
|
||||||
|
"name": "jap_web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "jap_web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@bokuweb/zstd-wasm": "^0.0.27",
|
||||||
|
"wa-sqlite": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vite": "^8.0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bokuweb/zstd-wasm": {
|
||||||
|
"version": "0.0.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bokuweb/zstd-wasm/-/zstd-wasm-0.0.27.tgz",
|
||||||
|
"integrity": "sha512-GDm2uOTK3ESjnYmSeLQifJnBsRCWajKLvN32D2ZcQaaCIJI/Hse9s74f7APXjHit95S10UImsRGkTsbwHmrtmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxc-project/types": {
|
||||||
|
"version": "0.133.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||||
|
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "1.10.0",
|
||||||
|
"@emnapi/runtime": "1.10.0",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/pluginutils": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
|
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-android-arm64": "1.32.0",
|
||||||
|
"lightningcss-darwin-arm64": "1.32.0",
|
||||||
|
"lightningcss-darwin-x64": "1.32.0",
|
||||||
|
"lightningcss-freebsd-x64": "1.32.0",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||||
|
"lightningcss-linux-x64-musl": "1.32.0",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-android-arm64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.12",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rolldown": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oxc-project/types": "=0.133.0",
|
||||||
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rolldown": "bin/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rolldown/binding-android-arm64": "1.0.3",
|
||||||
|
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||||
|
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||||
|
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||||
|
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||||
|
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||||
|
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||||
|
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "8.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lightningcss": "^1.32.0",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"postcss": "^8.5.15",
|
||||||
|
"rolldown": "1.0.3",
|
||||||
|
"tinyglobby": "^0.2.17"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
|
"@vitejs/devtools": "^0.1.18",
|
||||||
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
|
"jiti": ">=1.21.0",
|
||||||
|
"less": "^4.0.0",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-embedded": "^1.70.0",
|
||||||
|
"stylus": ">=0.54.8",
|
||||||
|
"sugarss": "^5.0.0",
|
||||||
|
"terser": "^5.16.0",
|
||||||
|
"tsx": "^4.8.1",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitejs/devtools": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jiti": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tsx": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wa-sqlite": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wa-sqlite/-/wa-sqlite-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kyybo5/BaJp76z7gDWGk2J6Hthl4NIPsE+swgraEjy3IY6r5zIR02wAs1OJH4XtJp1y3puj3Onp5eMGS0z7nUA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "jap_web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vite": "^8.0.12"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bokuweb/zstd-wasm": "^0.0.27",
|
||||||
|
"wa-sqlite": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||||
|
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||||
|
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||||
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,308 @@
|
|||||||
|
// ──── Constants ────
|
||||||
|
const MAX_HISTORY_LEN = 20;
|
||||||
|
const MAX_HISTORY_LEN_PART = 0.33;
|
||||||
|
const MAX_SCORE = 25;
|
||||||
|
const FADE_PER_DAY = 0.95;
|
||||||
|
|
||||||
|
// ──── Data types ────
|
||||||
|
|
||||||
|
export interface WordData {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
tags: string;
|
||||||
|
additional: Record<string, string>;
|
||||||
|
group_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WordGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardStatistics {
|
||||||
|
id: number;
|
||||||
|
word_id: number;
|
||||||
|
set_id: number;
|
||||||
|
last_open: number; // unix timestamp ms
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WordOpenMode = 'easy' | 'ok' | 'hard' | 'none';
|
||||||
|
|
||||||
|
export const SetOrderMode = {
|
||||||
|
Default: 'Default',
|
||||||
|
TrainWorstFirst: 'TrainWorstFirst',
|
||||||
|
FullRandom: 'FullRandom',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SetOrderMode = (typeof SetOrderMode)[keyof typeof SetOrderMode];
|
||||||
|
|
||||||
|
export interface CardSetSettings {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
forward: string;
|
||||||
|
backward: string;
|
||||||
|
filter: string;
|
||||||
|
count: number | null;
|
||||||
|
worst_words_list: WordData[] | null;
|
||||||
|
open_mode: SetOrderMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── CardStatistics helpers ────
|
||||||
|
|
||||||
|
export function calculatedScore(stat: CardStatistics): number {
|
||||||
|
const now = Date.now();
|
||||||
|
const diffMs = now - stat.last_open;
|
||||||
|
const days = diffMs / (1000 * 60 * 60 * 24);
|
||||||
|
const multiplier = Math.pow(FADE_PER_DAY, Math.max(0, days));
|
||||||
|
return stat.score * multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateStatScore(stat: CardStatistics, status: WordOpenMode): void {
|
||||||
|
const cs = calculatedScore(stat);
|
||||||
|
switch (status) {
|
||||||
|
case 'easy':
|
||||||
|
stat.score = Math.round(cs + 5);
|
||||||
|
break;
|
||||||
|
case 'ok':
|
||||||
|
stat.score = Math.round(cs + 2);
|
||||||
|
break;
|
||||||
|
case 'hard':
|
||||||
|
stat.score = Math.round(cs - 1);
|
||||||
|
break;
|
||||||
|
case 'none':
|
||||||
|
stat.score = Math.round(cs * 0.5);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.score < 1) stat.score = 1;
|
||||||
|
else if (stat.score > MAX_SCORE) stat.score = MAX_SCORE;
|
||||||
|
|
||||||
|
stat.last_open = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── WeightedIndex (simple O(n) implementation) ────
|
||||||
|
|
||||||
|
class WeightedIndex {
|
||||||
|
private weights: number[];
|
||||||
|
|
||||||
|
constructor(weights: number[]) {
|
||||||
|
this.weights = [...weights];
|
||||||
|
}
|
||||||
|
|
||||||
|
sample(): number {
|
||||||
|
const total = this.weights.reduce((a, b) => a + b, 0);
|
||||||
|
let r = Math.random() * total;
|
||||||
|
for (let i = 0; i < this.weights.length; i++) {
|
||||||
|
r -= this.weights[i];
|
||||||
|
if (r <= 0) return i;
|
||||||
|
}
|
||||||
|
return this.weights.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWeight(index: number, weight: number): void {
|
||||||
|
this.weights[index] = weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── SRSModule interface ────
|
||||||
|
|
||||||
|
interface SRSModule {
|
||||||
|
next(set: { words: WordData[]; set: CardStatistics[] }): number;
|
||||||
|
open(status: WordOpenMode, index: number, updatedWord: CardStatistics): void;
|
||||||
|
init(set: { words: WordData[]; set: CardStatistics[] }): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── RandomSRSModule ────
|
||||||
|
|
||||||
|
class RandomSRSModule implements SRSModule {
|
||||||
|
private backet: number[] = [];
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
next(set: { words: WordData[]; set: CardStatistics[] }): number {
|
||||||
|
if (this.backet.length === 0) {
|
||||||
|
this.backet = Array.from({ length: set.words.length }, (_, i) => i);
|
||||||
|
this.shuffle(this.backet);
|
||||||
|
}
|
||||||
|
return this.backet.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(_status: WordOpenMode, _index: number, _updatedWord: CardStatistics): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
init(set: { words: WordData[]; set: CardStatistics[] }): void {
|
||||||
|
this.initialized = true;
|
||||||
|
this.backet = Array.from({ length: set.words.length }, (_, i) => i);
|
||||||
|
this.shuffle(this.backet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private shuffle(arr: number[]): void {
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── SemiRandomSRSModule ────
|
||||||
|
|
||||||
|
class SemiRandomSRSModule implements SRSModule {
|
||||||
|
private history: number[] = [];
|
||||||
|
private lastWeights!: WeightedIndex;
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
next(set: { words: WordData[]; set: CardStatistics[] }): number {
|
||||||
|
const index = this.lastWeights.sample();
|
||||||
|
|
||||||
|
if (this.history.includes(index)) {
|
||||||
|
return this.next(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.history.length >= this.historyLen(set.set.length)) {
|
||||||
|
this.history.shift();
|
||||||
|
}
|
||||||
|
this.history.push(index);
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(_status: WordOpenMode, index: number, word: CardStatistics): void {
|
||||||
|
const newWeight = Math.pow(100.0 / Math.max(0.1, calculatedScore(word)), 2.0);
|
||||||
|
this.lastWeights.updateWeight(index, newWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(set: { words: WordData[]; set: CardStatistics[] }): void {
|
||||||
|
this.initialized = true;
|
||||||
|
const weights = set.set.map(
|
||||||
|
(s) => Math.pow(100.0 / Math.max(0.1, calculatedScore(s)), 2.0) * 2.0,
|
||||||
|
);
|
||||||
|
this.lastWeights = new WeightedIndex(weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
private historyLen(setLen: number): number {
|
||||||
|
return Math.min(MAX_HISTORY_LEN, Math.floor(setLen * MAX_HISTORY_LEN_PART));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── WorstWordsSRSModule (stub) ────
|
||||||
|
|
||||||
|
class WorstWordsSRSModule implements SRSModule {
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
next(_set: { words: WordData[]; set: CardStatistics[] }): number {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
open(_status: WordOpenMode, _index: number, _updatedWord: CardStatistics): void {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_set: { words: WordData[]; set: CardStatistics[] }): void {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── OrderModule ────
|
||||||
|
|
||||||
|
type OrderModule =
|
||||||
|
| { type: 'SemiRandomSRS'; module: SemiRandomSRSModule }
|
||||||
|
| { type: 'RandomSRS'; module: RandomSRSModule }
|
||||||
|
| { type: 'WorstWordsSRS'; module: WorstWordsSRSModule };
|
||||||
|
|
||||||
|
// ──── CardSet ────
|
||||||
|
|
||||||
|
export class CardSet {
|
||||||
|
words: WordData[];
|
||||||
|
set: CardStatistics[];
|
||||||
|
currentWordIndex: number | null = null;
|
||||||
|
private orderModule: OrderModule;
|
||||||
|
|
||||||
|
constructor(settings: CardSetSettings, words: WordData[], stats: CardStatistics[]) {
|
||||||
|
this.words = words;
|
||||||
|
this.set = stats;
|
||||||
|
|
||||||
|
const openMode = settings.open_mode;
|
||||||
|
|
||||||
|
this.orderModule =
|
||||||
|
openMode === SetOrderMode.FullRandom
|
||||||
|
? { type: 'RandomSRS', module: new RandomSRSModule() }
|
||||||
|
: openMode === SetOrderMode.TrainWorstFirst
|
||||||
|
? { type: 'WorstWordsSRS', module: new WorstWordsSRSModule() }
|
||||||
|
: { type: 'SemiRandomSRS', module: new SemiRandomSRSModule() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrderContext() {
|
||||||
|
return { words: this.words, set: this.set };
|
||||||
|
}
|
||||||
|
|
||||||
|
next(): { word: WordData; stat: CardStatistics } {
|
||||||
|
const ctx = this.getOrderContext();
|
||||||
|
|
||||||
|
let index: number;
|
||||||
|
const mod = this.orderModule;
|
||||||
|
|
||||||
|
switch (mod.type) {
|
||||||
|
case 'SemiRandomSRS': {
|
||||||
|
if (!mod.module['initialized']) {
|
||||||
|
mod.module.init(ctx);
|
||||||
|
}
|
||||||
|
index = mod.module.next(ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'RandomSRS': {
|
||||||
|
if (!mod.module['initialized']) {
|
||||||
|
mod.module.init(ctx);
|
||||||
|
}
|
||||||
|
index = mod.module.next(ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'WorstWordsSRS': {
|
||||||
|
if (!mod.module['initialized']) {
|
||||||
|
mod.module.init(ctx);
|
||||||
|
}
|
||||||
|
index = mod.module.next(ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentWordIndex = index;
|
||||||
|
return { word: this.words[index], stat: this.set[index] };
|
||||||
|
}
|
||||||
|
|
||||||
|
open(status: WordOpenMode): void {
|
||||||
|
if (this.currentWordIndex === null) return;
|
||||||
|
const index = this.currentWordIndex;
|
||||||
|
|
||||||
|
const word = this.set[index];
|
||||||
|
updateStatScore(word, status);
|
||||||
|
|
||||||
|
const mod = this.orderModule;
|
||||||
|
switch (mod.type) {
|
||||||
|
case 'SemiRandomSRS':
|
||||||
|
mod.module.open(status, index, word);
|
||||||
|
break;
|
||||||
|
case 'RandomSRS':
|
||||||
|
mod.module.open(status, index, word);
|
||||||
|
break;
|
||||||
|
case 'WorstWordsSRS':
|
||||||
|
mod.module.open(status, index, word);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.set.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──── Factory helper ────
|
||||||
|
|
||||||
|
export function createCardSet(
|
||||||
|
settings: CardSetSettings,
|
||||||
|
words: WordData[],
|
||||||
|
stats: CardStatistics[],
|
||||||
|
): CardSet {
|
||||||
|
return new CardSet(settings, words, stats);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="32" height="32" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"/><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
export function setupCounter(element: HTMLButtonElement) {
|
||||||
|
let counter = 0
|
||||||
|
const setCounter = (count: number) => {
|
||||||
|
counter = count
|
||||||
|
element.innerHTML = `Count is ${counter}`
|
||||||
|
}
|
||||||
|
element.addEventListener('click', () => setCounter(counter + 1))
|
||||||
|
setCounter(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { execute } from './sqlite-manager.ts';
|
||||||
|
|
||||||
|
export interface Word {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
tags: string;
|
||||||
|
more: string | null;
|
||||||
|
group_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardSetRecord {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
forward: string;
|
||||||
|
backward: string;
|
||||||
|
filter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все колоды (card_set).
|
||||||
|
*/
|
||||||
|
export async function getCardSets(): Promise<CardSetRecord[]> {
|
||||||
|
const rows: any[] = await execute(`
|
||||||
|
SELECT id, name, forward, backward, filter
|
||||||
|
FROM card_set
|
||||||
|
ORDER BY id ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return rows.map((row: any[]) => ({
|
||||||
|
id: row[0],
|
||||||
|
name: row[1],
|
||||||
|
forward: row[2],
|
||||||
|
backward: row[3],
|
||||||
|
filter: row[4],
|
||||||
|
})) as CardSetRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все слова для повторения.
|
||||||
|
*/
|
||||||
|
export async function getWordsForReview(): Promise<Word[]> {
|
||||||
|
const rows: any[] = await execute(`
|
||||||
|
SELECT id, key, value, tags, more, group_id
|
||||||
|
FROM words
|
||||||
|
ORDER BY id ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return rows.map((row: any[]) => ({
|
||||||
|
id: row[0],
|
||||||
|
key: row[1],
|
||||||
|
value: row[2],
|
||||||
|
tags: row[3],
|
||||||
|
more: row[4],
|
||||||
|
group_id: row[5],
|
||||||
|
})) as Word[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Web Worker для работы с wa-sqlite + OPFS.
|
||||||
|
* Воркер необходим, так как createSyncAccessHandle() доступен только в Worker'ах.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let sqlite: any = null;
|
||||||
|
let db: number | null = null;
|
||||||
|
let vfs: any = null;
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
const DB_FILENAME = '/japanese_app.db';
|
||||||
|
const SQLITE_OPEN_CREATE = 0x0004;
|
||||||
|
const SQLITE_OPEN_READWRITE = 0x0002;
|
||||||
|
const OPEN_FLAGS = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
if (initialized) return;
|
||||||
|
|
||||||
|
const { Factory } = await import('wa-sqlite/src/sqlite-api.js');
|
||||||
|
const { OriginPrivateFileSystemVFS } = await import(
|
||||||
|
'wa-sqlite/src/examples/OriginPrivateFileSystemVFS.js'
|
||||||
|
);
|
||||||
|
const ModuleConstructor = (await import('wa-sqlite/dist/wa-sqlite-async.mjs')).default;
|
||||||
|
|
||||||
|
const module = await ModuleConstructor();
|
||||||
|
const api = Factory(module);
|
||||||
|
sqlite = api;
|
||||||
|
|
||||||
|
const opfsVfs = new OriginPrivateFileSystemVFS();
|
||||||
|
vfs = opfsVfs;
|
||||||
|
|
||||||
|
const rc = api.vfs_register(opfsVfs, true);
|
||||||
|
if (rc) {
|
||||||
|
throw new Error(`Не удалось зарегистрировать OPFS VFS, код ${rc}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = await api.open_v2(DB_FILENAME, OPEN_FLAGS);
|
||||||
|
db = handle;
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
self.postMessage({ type: 'ready', ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execSchema() {
|
||||||
|
if (!sqlite || db === null) return;
|
||||||
|
|
||||||
|
const schema = `
|
||||||
|
create table if not exists card_set
|
||||||
|
(
|
||||||
|
id INTEGER primary key autoincrement,
|
||||||
|
name TEXT not null,
|
||||||
|
forward TEXT not null,
|
||||||
|
backward TEXT not null,
|
||||||
|
filter TEXT not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists settings
|
||||||
|
(
|
||||||
|
id text primary key,
|
||||||
|
value text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists word_group
|
||||||
|
(
|
||||||
|
id INTEGER primary key autoincrement,
|
||||||
|
name TEXT not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists words
|
||||||
|
(
|
||||||
|
id INTEGER primary key autoincrement,
|
||||||
|
key TEXT not null,
|
||||||
|
value TEXT not null,
|
||||||
|
tags TEXT not null,
|
||||||
|
more TEXT,
|
||||||
|
group_id integer default 1 not null
|
||||||
|
constraint words_word_group_id_fk references word_group on update cascade on delete cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists card_stats
|
||||||
|
(
|
||||||
|
id INTEGER primary key autoincrement,
|
||||||
|
word_id INTEGER not null references words on delete cascade,
|
||||||
|
set_id TEXT not null references card_set on delete cascade,
|
||||||
|
score INTEGER default 1 not null,
|
||||||
|
last_opened integer not null
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const statements = schema
|
||||||
|
.split(';')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0);
|
||||||
|
|
||||||
|
for (const stmt of statements) {
|
||||||
|
await sqlite.exec(db!, stmt + ';');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async (e: MessageEvent) => {
|
||||||
|
const { type, id, sql, params } = e.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'init': {
|
||||||
|
await init();
|
||||||
|
await execSchema();
|
||||||
|
self.postMessage({ type: 'result', id, ok: true, data: null });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'execute': {
|
||||||
|
if (!sqlite || db === null) throw new Error('БД не инициализирована');
|
||||||
|
const result = await sqlite.execWithParams(db, sql, params);
|
||||||
|
self.postMessage({ type: 'result', id, ok: true, data: result.rows });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'executeVoid': {
|
||||||
|
if (!sqlite || db === null) throw new Error('БД не инициализирована');
|
||||||
|
await sqlite.run(db, sql, params);
|
||||||
|
self.postMessage({ type: 'result', id, ok: true, data: null });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'closeDb': {
|
||||||
|
if (sqlite && db !== null) {
|
||||||
|
await sqlite.close(db);
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
if (vfs) {
|
||||||
|
await vfs.close();
|
||||||
|
vfs = null;
|
||||||
|
}
|
||||||
|
initialized = false;
|
||||||
|
self.postMessage({ type: 'result', id, ok: true, data: null });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'getFile': {
|
||||||
|
// Получаем файл из OPFS
|
||||||
|
const root = await navigator.storage.getDirectory();
|
||||||
|
const fileHandle = await root.getFileHandle('japanese_app.db');
|
||||||
|
const file = await fileHandle.getFile();
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
// Передаём ArrayBuffer как Transferable
|
||||||
|
self.postMessage(
|
||||||
|
{ type: 'result', id, ok: true, data: buffer },
|
||||||
|
{ transfer: [buffer] },
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'replaceFile': {
|
||||||
|
// Закрываем текущее соединение
|
||||||
|
if (sqlite && db !== null) {
|
||||||
|
await sqlite.close(db);
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
if (vfs) {
|
||||||
|
await vfs.close();
|
||||||
|
vfs = null;
|
||||||
|
}
|
||||||
|
initialized = false;
|
||||||
|
|
||||||
|
// Перезаписываем файл в OPFS
|
||||||
|
const root = await navigator.storage.getDirectory();
|
||||||
|
const fileHandle = await root.getFileHandle('japanese_app.db', { create: true });
|
||||||
|
const accessHandle = await (fileHandle as any).createSyncAccessHandle();
|
||||||
|
try {
|
||||||
|
const data = new Uint8Array(params);
|
||||||
|
accessHandle.truncate(0);
|
||||||
|
accessHandle.write(data, { at: 0 });
|
||||||
|
accessHandle.flush();
|
||||||
|
} finally {
|
||||||
|
accessHandle.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переинициализируем
|
||||||
|
await init();
|
||||||
|
self.postMessage({ type: 'result', id, ok: true, data: null });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Неизвестный тип сообщения: ${type}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
self.postMessage({ type: 'result', id, ok: false, error: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// ──── Database schema ────
|
||||||
|
// Imported from db_schema.sql
|
||||||
|
|
||||||
|
export function getSchema(): string {
|
||||||
|
return `
|
||||||
|
create table card_set
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
name TEXT not null,
|
||||||
|
forward TEXT not null,
|
||||||
|
backward TEXT not null,
|
||||||
|
filter TEXT not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table settings
|
||||||
|
(
|
||||||
|
id text
|
||||||
|
primary key,
|
||||||
|
value text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table word_group
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
name TEXT not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table words
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
key TEXT not null,
|
||||||
|
value TEXT not null,
|
||||||
|
tags TEXT not null,
|
||||||
|
more TEXT,
|
||||||
|
group_id integer default 1 not null
|
||||||
|
constraint words_word_group_id_fk
|
||||||
|
references word_group
|
||||||
|
on update cascade on delete cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create table card_stats
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
word_id INTEGER not null
|
||||||
|
references words
|
||||||
|
on delete cascade,
|
||||||
|
set_id TEXT not null
|
||||||
|
references card_set
|
||||||
|
on delete cascade,
|
||||||
|
score INTEGER default 1 not null,
|
||||||
|
last_opened integer not null
|
||||||
|
);
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
import './style.css';
|
||||||
|
import { initDB, closeDb, getDatabaseFile, replaceDatabaseFile } from './sqlite-manager.ts';
|
||||||
|
import { uploadDatabase, downloadDatabase } from './sync.ts';
|
||||||
|
import { getWordsForReview, getCardSets } from './db-manager.ts';
|
||||||
|
import type { CardSetRecord } from './db-manager.ts';
|
||||||
|
import type { Word } from './db-manager.ts';
|
||||||
|
import { SetOrderMode } from './algorithm.ts';
|
||||||
|
|
||||||
|
// ----- Конфигурация -----
|
||||||
|
const API_BASE_URL = 'https://learning.micialware.ru';
|
||||||
|
|
||||||
|
// ----- Элементы UI -----
|
||||||
|
const container = document.getElementById('page-container') as HTMLElement;
|
||||||
|
const navBtns = document.querySelectorAll<HTMLButtonElement>('.nav-btn');
|
||||||
|
let currentPage: 'words' | 'sets' = 'words';
|
||||||
|
|
||||||
|
// ----- Вспомогательные функции -----
|
||||||
|
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(msg: string, isError = false) {
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = msg;
|
||||||
|
statusEl.className = 'status-bar';
|
||||||
|
if (isError) statusEl.classList.add('error');
|
||||||
|
else statusEl.classList.add('success');
|
||||||
|
}
|
||||||
|
console.log(`[App] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSyncId(): string {
|
||||||
|
const input = document.getElementById('syncId') as HTMLInputElement | null;
|
||||||
|
const id = input?.value.trim();
|
||||||
|
if (!id) throw new Error('Введите ID синхронизации');
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Рендеринг страниц -----
|
||||||
|
|
||||||
|
function renderWordsPage() {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title">📡 Синхронизация</h2>
|
||||||
|
<div class="sync-input-group">
|
||||||
|
<input type="text" id="syncId" placeholder="Введите ID синхронизации" autocomplete="off" spellcheck="false" />
|
||||||
|
<div class="sync-buttons">
|
||||||
|
<button id="btnDownload" class="btn">📥 Скачать</button>
|
||||||
|
<button id="btnUpload" class="btn btn-primary">📤 Загрузить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="flex: 1;">
|
||||||
|
<h2 class="card-title">📚 Слова</h2>
|
||||||
|
<ul id="word-list" class="word-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" class="status-bar">Загрузка...</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWordsList(words: Word[]) {
|
||||||
|
const wordListEl = document.getElementById('word-list') as HTMLUListElement | null;
|
||||||
|
if (!wordListEl) return;
|
||||||
|
|
||||||
|
wordListEl.innerHTML = '';
|
||||||
|
|
||||||
|
if (words.length === 0) {
|
||||||
|
wordListEl.innerHTML = '<li class="word-item word-item-empty">Нет слов для повторения 🎉</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const word of words) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'word-item';
|
||||||
|
|
||||||
|
let reading = word.key;
|
||||||
|
let translation = word.value;
|
||||||
|
|
||||||
|
if (word.more) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(word.more);
|
||||||
|
reading = parsed.reading || word.key;
|
||||||
|
translation = parsed.translation || word.value;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.innerHTML = `
|
||||||
|
<span class="word-key">${reading}</span>
|
||||||
|
<span class="word-value">${escapeHtml(translation)}</span>
|
||||||
|
${word.tags ? `<span class="word-tags">${escapeHtml(word.tags)}</span>` : ''}
|
||||||
|
`;
|
||||||
|
wordListEl.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSetsPage() {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="card" style="flex:1; display:flex; flex-direction:column; padding:0; overflow:hidden;">
|
||||||
|
<div class="sets-layout" style="flex:1;">
|
||||||
|
<ul id="sets-list" class="sets-list"></ul>
|
||||||
|
<div id="sets-detail" class="sets-detail">
|
||||||
|
<div class="placeholder-area">
|
||||||
|
<div class="placeholder-icon">🗂️</div>
|
||||||
|
<div>Выберите колоду</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="status" class="status-bar">Загрузка...</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSetsList(sets: CardSetRecord[]) {
|
||||||
|
const listEl = document.getElementById('sets-list') as HTMLUListElement | null;
|
||||||
|
if (!listEl) return;
|
||||||
|
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
|
||||||
|
if (sets.length === 0) {
|
||||||
|
listEl.innerHTML = '<li style="opacity:0.5;text-align:center;padding:20px;">Нет колод</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const set of sets) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = set.name;
|
||||||
|
li.dataset.setId = String(set.id);
|
||||||
|
li.addEventListener('click', () => showSetDetail(set, li));
|
||||||
|
listEl.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выделяем первую
|
||||||
|
const first = listEl.querySelector('li') as HTMLLIElement | null;
|
||||||
|
if (first) {
|
||||||
|
first.classList.add('selected');
|
||||||
|
const id = Number(first.dataset.setId);
|
||||||
|
const set = sets.find(s => s.id === id);
|
||||||
|
if (set) showSetDetail(set, first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSetDetail(set: CardSetRecord, listItem: HTMLLIElement) {
|
||||||
|
// Обновляем выделение
|
||||||
|
document.querySelectorAll('#sets-list li').forEach(el => el.classList.remove('selected'));
|
||||||
|
listItem.classList.add('selected');
|
||||||
|
|
||||||
|
const detailEl = document.getElementById('sets-detail');
|
||||||
|
if (!detailEl) return;
|
||||||
|
|
||||||
|
detailEl.innerHTML = `
|
||||||
|
<h3>${escapeHtml(set.name)}</h3>
|
||||||
|
<p><strong>Прямой порядок:</strong> ${escapeHtml(set.forward)}</p>
|
||||||
|
<p><strong>Обратный порядок:</strong> ${escapeHtml(set.backward)}</p>
|
||||||
|
<p><strong>Фильтр:</strong> ${escapeHtml(set.filter)}</p>
|
||||||
|
|
||||||
|
<label style="font-size:0.8rem;color:var(--text);display:block;margin-top:12px;">Режим тренировки</label>
|
||||||
|
<select id="mode-select" class="mode-select">
|
||||||
|
<option value="${SetOrderMode.Default}">${SetOrderMode.Default} — взвешенная случайность</option>
|
||||||
|
<option value="${SetOrderMode.TrainWorstFirst}">${SetOrderMode.TrainWorstFirst} — слабые слова</option>
|
||||||
|
<option value="${SetOrderMode.FullRandom}">${SetOrderMode.FullRandom} — полная случайность</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" data-set-id="${set.id}">▶ Начать тренировку</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Навигация -----
|
||||||
|
|
||||||
|
function switchPage(page: 'words' | 'sets') {
|
||||||
|
currentPage = page;
|
||||||
|
|
||||||
|
// Обновляем активную кнопку
|
||||||
|
navBtns.forEach(btn => {
|
||||||
|
btn.classList.toggle('active', btn.dataset.page === page);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (page === 'words') {
|
||||||
|
renderWordsPage();
|
||||||
|
loadWords();
|
||||||
|
} else {
|
||||||
|
renderSetsPage();
|
||||||
|
loadSets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWords() {
|
||||||
|
try {
|
||||||
|
const words = await getWordsForReview();
|
||||||
|
renderWordsList(words);
|
||||||
|
const count = words.length;
|
||||||
|
setStatus(`Слов: ${count}`);
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('Ошибка загрузки слов', true);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSets() {
|
||||||
|
try {
|
||||||
|
const sets = await getCardSets();
|
||||||
|
renderSetsList(sets);
|
||||||
|
setStatus(`Колод: ${sets.length}`);
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('Ошибка загрузки колод', true);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Обработчики синхронизации -----
|
||||||
|
|
||||||
|
async function onDownload() {
|
||||||
|
try {
|
||||||
|
const id = getSyncId();
|
||||||
|
const btn = document.getElementById('btnDownload') as HTMLButtonElement | null;
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
|
setStatus('Скачивание...');
|
||||||
|
await downloadDatabase(id, API_BASE_URL, replaceDatabaseFile, initDB);
|
||||||
|
|
||||||
|
if (currentPage === 'words') await loadWords();
|
||||||
|
setStatus('База данных скачана.');
|
||||||
|
} catch {
|
||||||
|
// ошибка уже обработана в sync.ts
|
||||||
|
} finally {
|
||||||
|
const btn = document.getElementById('btnDownload') as HTMLButtonElement | null;
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUpload() {
|
||||||
|
try {
|
||||||
|
const id = getSyncId();
|
||||||
|
const btn = document.getElementById('btnUpload') as HTMLButtonElement | null;
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
|
setStatus('Загрузка...');
|
||||||
|
await uploadDatabase(id, API_BASE_URL, closeDb, getDatabaseFile);
|
||||||
|
|
||||||
|
await initDB();
|
||||||
|
if (currentPage === 'words') await loadWords();
|
||||||
|
setStatus('База данных загружена.');
|
||||||
|
} catch {
|
||||||
|
// ошибка уже обработана в sync.ts
|
||||||
|
} finally {
|
||||||
|
const btn = document.getElementById('btnUpload') as HTMLButtonElement | null;
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Инициализация -----
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await initDB();
|
||||||
|
console.log('[App] База данных инициализирована.');
|
||||||
|
|
||||||
|
// Навигация
|
||||||
|
navBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
switchPage(btn.dataset.page as 'words' | 'sets');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Стартовая страница
|
||||||
|
switchPage('words');
|
||||||
|
|
||||||
|
// Глобальные обработчики кликов (для кнопок на динамических страницах)
|
||||||
|
container.addEventListener('click', async (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.id === 'btnDownload') await onDownload();
|
||||||
|
if (target.id === 'btnUpload') await onUpload();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[App] Ошибка инициализации:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import DbWorker from './db-worker.ts?worker';
|
||||||
|
|
||||||
|
// ----- Прокси к Worker -----
|
||||||
|
|
||||||
|
let worker: Worker | null = null;
|
||||||
|
let initPromise: Promise<void> | null = null;
|
||||||
|
let requestId = 0;
|
||||||
|
|
||||||
|
type WorkerRequest = {
|
||||||
|
resolve: (data: any) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pending = new Map<number, WorkerRequest>();
|
||||||
|
|
||||||
|
function sendToWorker(type: string, payload?: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!worker) {
|
||||||
|
reject(new Error('Worker не создан'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = ++requestId;
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
|
||||||
|
worker.postMessage({ type, id, ...payload });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWorker(): Promise<void> {
|
||||||
|
if (initPromise) return initPromise;
|
||||||
|
|
||||||
|
initPromise = new Promise((resolve, reject) => {
|
||||||
|
worker = new DbWorker();
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent) => {
|
||||||
|
const msg = e.data;
|
||||||
|
|
||||||
|
if (msg.type === 'ready') {
|
||||||
|
// Готов — запрос init уже обработан
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'result') {
|
||||||
|
const pendingReq = pending.get(msg.id);
|
||||||
|
if (!pendingReq) return;
|
||||||
|
pending.delete(msg.id);
|
||||||
|
|
||||||
|
if (msg.ok) {
|
||||||
|
pendingReq.resolve(msg.data);
|
||||||
|
} else {
|
||||||
|
pendingReq.reject(new Error(msg.error || 'Неизвестная ошибка Worker'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (err) => {
|
||||||
|
console.error('[SQLite Worker] Ошибка:', err);
|
||||||
|
reject(new Error('Ошибка Worker'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем init
|
||||||
|
const id = ++requestId;
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
worker.postMessage({ type: 'init', id });
|
||||||
|
});
|
||||||
|
|
||||||
|
return initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Публичные функции -----
|
||||||
|
|
||||||
|
export async function initDB(): Promise<void> {
|
||||||
|
await initWorker();
|
||||||
|
console.log('[SQLite] База данных инициализирована.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function execute(sql: string, params?: any[]): Promise<any[]> {
|
||||||
|
const result = await sendToWorker('execute', { sql, params });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeVoid(sql: string, params?: any[]): Promise<void> {
|
||||||
|
await sendToWorker('executeVoid', { sql, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDatabaseFile(): Promise<ArrayBuffer> {
|
||||||
|
const buffer = await sendToWorker('getFile');
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Закрывает соединение с БД и освобождает блокировку файла OPFS.
|
||||||
|
*/
|
||||||
|
export async function closeDb(): Promise<void> {
|
||||||
|
if (!worker) return;
|
||||||
|
await sendToWorker('closeDb');
|
||||||
|
worker.terminate();
|
||||||
|
worker = null;
|
||||||
|
initPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function replaceDatabaseFile(arrayBuffer: ArrayBuffer): Promise<void> {
|
||||||
|
// ArrayBuffer передаётся через transferable
|
||||||
|
await sendToWorkerRaw('replaceFile', arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendToWorkerRaw(type: string, buffer: ArrayBuffer): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!worker) {
|
||||||
|
reject(new Error('Worker не создан'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = ++requestId;
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
|
||||||
|
worker.postMessage({ type, id, params: buffer }, [buffer]);
|
||||||
|
});
|
||||||
|
}
|
||||||
+404
@@ -0,0 +1,404 @@
|
|||||||
|
:root {
|
||||||
|
--text: #6b6375;
|
||||||
|
--text-h: #08060d;
|
||||||
|
--bg: #fff;
|
||||||
|
--card-bg: #f8f7f9;
|
||||||
|
--border: #e5e4e7;
|
||||||
|
--accent: #7c3aed;
|
||||||
|
--accent-light: rgba(124, 58, 237, 0.1);
|
||||||
|
--success: #16a34a;
|
||||||
|
--error: #dc2626;
|
||||||
|
--shadow: rgba(0, 0, 0, 0.08) 0 4px 12px;
|
||||||
|
|
||||||
|
--sans: -apple-system, 'Segoe UI', system-ui, Roboto, sans-serif;
|
||||||
|
--mono: ui-monospace, Consolas, monospace;
|
||||||
|
|
||||||
|
font: 16px/1.5 var(--sans);
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text: #9ca3af;
|
||||||
|
--text-h: #f3f4f6;
|
||||||
|
--bg: #0f0f13;
|
||||||
|
--card-bg: #1a1b23;
|
||||||
|
--border: #2e303a;
|
||||||
|
--accent: #a78bfa;
|
||||||
|
--accent-light: rgba(167, 139, 250, 0.15);
|
||||||
|
--shadow: rgba(0, 0, 0, 0.4) 0 4px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 1.5rem 1.25rem 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-h);
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-input-group input {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-h);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-input-group input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-h);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--accent-light);
|
||||||
|
color: var(--text);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.error {
|
||||||
|
background: rgba(220, 38, 38, 0.1);
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.success {
|
||||||
|
background: rgba(22, 163, 74, 0.1);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.4;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-item-empty {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-key {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-h);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-value {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text);
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-tags {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-light);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-basis: 100%;
|
||||||
|
order: 1;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──── Навигация ──── */
|
||||||
|
|
||||||
|
.app-nav {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--card-bg);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
flex: 1;
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.active {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──── Страница колод ──── */
|
||||||
|
|
||||||
|
.sets-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-list {
|
||||||
|
flex: 1;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-list li {
|
||||||
|
padding: 12px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
transition: background 0.15s;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-list li:hover {
|
||||||
|
background: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-list li.selected {
|
||||||
|
background: var(--accent-light);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-detail {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-detail h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-detail p {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-detail .mode-select {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-h);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets-detail .btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.sets-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sets-list {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
import { init, compress, decompress } from '@bokuweb/zstd-wasm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает (upload) базу данных на сервер.
|
||||||
|
*
|
||||||
|
* @param id - Идентификатор для синхронизации.
|
||||||
|
* @param apiBaseUrl - Базовый URL API (без слеша на конце).
|
||||||
|
* @param closeDb - Функция для закрытия соединения с БД.
|
||||||
|
* @param getDbBuffer - Функция, возвращающая ArrayBuffer с бинарным файлом БД.
|
||||||
|
*/
|
||||||
|
export async function uploadDatabase(
|
||||||
|
id: string,
|
||||||
|
apiBaseUrl: string,
|
||||||
|
closeDb: () => Promise<void>,
|
||||||
|
getDbBuffer: () => Promise<ArrayBuffer>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 1. Закрываем соединение, чтобы снять блокировку файла OPFS
|
||||||
|
await closeDb();
|
||||||
|
|
||||||
|
// 2. Получаем бинарный файл БД
|
||||||
|
const dbBuffer = await getDbBuffer();
|
||||||
|
const plainBytes = new Uint8Array(dbBuffer);
|
||||||
|
|
||||||
|
// 3. Сжимаем zstd
|
||||||
|
await init();
|
||||||
|
const compressed = compress(plainBytes);
|
||||||
|
|
||||||
|
// 4. Создаём FormData и добавляем сжатый файл
|
||||||
|
const formData = new FormData();
|
||||||
|
const blob = new Blob([compressed.buffer as ArrayBuffer], { type: 'application/octet-stream' });
|
||||||
|
formData.append('db', blob, 'database.sqlite.zst');
|
||||||
|
|
||||||
|
// 5. POST-запрос
|
||||||
|
const url = `${apiBaseUrl.replace(/\/+$/, '')}/upload/${encodeURIComponent(id)}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка загрузки: сервер ответил ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Sync] База данных успешно загружена.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Sync] Ошибка загрузки:', err);
|
||||||
|
alert(`Ошибка загрузки: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скачивает (download) базу данных с сервера.
|
||||||
|
*
|
||||||
|
* @param id - Идентификатор для синхронизации.
|
||||||
|
* @param apiBaseUrl - Базовый URL API (без слеша на конце).
|
||||||
|
* @param replaceDbBuffer - Функция, перезаписывающая файл БД новым ArrayBuffer.
|
||||||
|
* @param initDb - Функция для повторной инициализации соединения с БД.
|
||||||
|
*/
|
||||||
|
export async function downloadDatabase(
|
||||||
|
id: string,
|
||||||
|
apiBaseUrl: string,
|
||||||
|
replaceDbBuffer: (buffer: ArrayBuffer) => Promise<void>,
|
||||||
|
initDb: () => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 1. GET-запрос
|
||||||
|
const url = `${apiBaseUrl.replace(/\/+$/, '')}/download/${encodeURIComponent(id)}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка скачивания: сервер ответил ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Получаем сжатые данные
|
||||||
|
const compressedBuffer = await response.arrayBuffer();
|
||||||
|
const compressedBytes = new Uint8Array(compressedBuffer);
|
||||||
|
|
||||||
|
// 3. Распаковываем zstd
|
||||||
|
await init();
|
||||||
|
const decompressedBytes = decompress(compressedBytes);
|
||||||
|
|
||||||
|
// 4. Перезаписываем файл в OPFS
|
||||||
|
await replaceDbBuffer(decompressedBytes.buffer as ArrayBuffer);
|
||||||
|
|
||||||
|
// 5. Переинициализируем соединение с БД
|
||||||
|
await initDb();
|
||||||
|
|
||||||
|
console.log('[Sync] База данных успешно скачана и применена.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Sync] Ошибка скачивания:', err);
|
||||||
|
alert(`Ошибка скачивания: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
declare module 'wa-sqlite/dist/wa-sqlite-async.mjs' {
|
||||||
|
export default function ModuleConstructor(options?: any): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'wa-sqlite/src/sqlite-api.js' {
|
||||||
|
export interface SQLiteAPI {
|
||||||
|
vfs_register(vfs: any, makeDefault: boolean): number;
|
||||||
|
open_v2(
|
||||||
|
filename: string,
|
||||||
|
flags: number,
|
||||||
|
vfs?: string,
|
||||||
|
): Promise<number>;
|
||||||
|
close(db: number): Promise<void>;
|
||||||
|
exec(
|
||||||
|
db: number,
|
||||||
|
sql: string,
|
||||||
|
callback?: (row: any, columns: string[]) => void,
|
||||||
|
): Promise<number>;
|
||||||
|
execWithParams(
|
||||||
|
db: number,
|
||||||
|
sql: string,
|
||||||
|
params?: any[] | Record<string, any>,
|
||||||
|
): Promise<{ rows: any[]; columns: string[] }>;
|
||||||
|
run(
|
||||||
|
db: number,
|
||||||
|
sql: string,
|
||||||
|
params?: any[] | Record<string, any>,
|
||||||
|
): Promise<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Factory(Module: any): SQLiteAPI;
|
||||||
|
|
||||||
|
export class SQLiteError extends Error {
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SQLITE_OPEN_READWRITE: number;
|
||||||
|
export const SQLITE_OPEN_CREATE: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'wa-sqlite/src/examples/OriginPrivateFileSystemVFS.js' {
|
||||||
|
class OriginPrivateFileSystemVFS {
|
||||||
|
constructor();
|
||||||
|
close(): Promise<void>;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export { OriginPrivateFileSystemVFS };
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: { new (): Worker };
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2023",
|
||||||
|
"module": "esnext",
|
||||||
|
"lib": ["ES2023", "DOM"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['wa-sqlite'],
|
||||||
|
},
|
||||||
|
assetsInclude: ['**/*.wasm'],
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user