✨ feat: 1. Add database migration function. 2. Add download speed statistics persistence and area chart display.
This commit is contained in:
parent
15c83639a7
commit
2de0e63c9a
33 changed files with 1932 additions and 103 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
package telegram.files;
|
package telegram.files;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Version;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.log.Log;
|
import cn.hutool.log.Log;
|
||||||
import cn.hutool.log.LogFactory;
|
import cn.hutool.log.LogFactory;
|
||||||
|
|
@ -9,11 +10,12 @@ import io.vertx.core.Promise;
|
||||||
import io.vertx.jdbcclient.JDBCConnectOptions;
|
import io.vertx.jdbcclient.JDBCConnectOptions;
|
||||||
import io.vertx.jdbcclient.JDBCPool;
|
import io.vertx.jdbcclient.JDBCPool;
|
||||||
import io.vertx.sqlclient.PoolOptions;
|
import io.vertx.sqlclient.PoolOptions;
|
||||||
import telegram.files.repository.FileRepository;
|
import io.vertx.sqlclient.SqlConnection;
|
||||||
import telegram.files.repository.SettingRepository;
|
import org.jooq.lambda.tuple.Tuple;
|
||||||
import telegram.files.repository.TelegramRepository;
|
import telegram.files.repository.*;
|
||||||
import telegram.files.repository.impl.FileRepositoryImpl;
|
import telegram.files.repository.impl.FileRepositoryImpl;
|
||||||
import telegram.files.repository.impl.SettingRepositoryImpl;
|
import telegram.files.repository.impl.SettingRepositoryImpl;
|
||||||
|
import telegram.files.repository.impl.StatisticRepositoryImpl;
|
||||||
import telegram.files.repository.impl.TelegramRepositoryImpl;
|
import telegram.files.repository.impl.TelegramRepositoryImpl;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -31,6 +33,8 @@ public class DataVerticle extends AbstractVerticle {
|
||||||
|
|
||||||
public static SettingRepository settingRepository;
|
public static SettingRepository settingRepository;
|
||||||
|
|
||||||
|
public static StatisticRepository statisticRepository;
|
||||||
|
|
||||||
public void start(Promise<Void> stopPromise) {
|
public void start(Promise<Void> stopPromise) {
|
||||||
pool = JDBCPool.pool(vertx,
|
pool = JDBCPool.pool(vertx,
|
||||||
new JDBCConnectOptions()
|
new JDBCConnectOptions()
|
||||||
|
|
@ -38,14 +42,26 @@ public class DataVerticle extends AbstractVerticle {
|
||||||
,
|
,
|
||||||
new PoolOptions().setMaxSize(16).setName("pool-tf")
|
new PoolOptions().setMaxSize(16).setName("pool-tf")
|
||||||
);
|
);
|
||||||
|
settingRepository = new SettingRepositoryImpl(pool);
|
||||||
telegramRepository = new TelegramRepositoryImpl(pool);
|
telegramRepository = new TelegramRepositoryImpl(pool);
|
||||||
fileRepository = new FileRepositoryImpl(pool);
|
fileRepository = new FileRepositoryImpl(pool);
|
||||||
settingRepository = new SettingRepositoryImpl(pool);
|
statisticRepository = new StatisticRepositoryImpl(pool);
|
||||||
Future.all(List.of(
|
List<Definition> definitions = List.of(
|
||||||
telegramRepository.init(),
|
new SettingRecord.SettingRecordDefinition(),
|
||||||
fileRepository.init(),
|
new TelegramRecord.TelegramRecordDefinition(),
|
||||||
settingRepository.init()
|
new FileRecord.FileRecordDefinition(),
|
||||||
))
|
new StatisticRecord.StatisticRecordDefinition()
|
||||||
|
);
|
||||||
|
pool.getConnection()
|
||||||
|
.compose(conn -> Future.all(definitions.stream().map(d -> d.createTable(conn)).toList()).map(conn))
|
||||||
|
.compose(conn -> settingRepository.<Version>getByKey(SettingKey.version).map(v -> Tuple.tuple(conn, v)))
|
||||||
|
.compose(tuple -> {
|
||||||
|
SqlConnection conn = tuple.v1;
|
||||||
|
Version version = tuple.v2 == null ? new Version("0.0.0") : tuple.v2;
|
||||||
|
return Future.all(definitions.stream().map(d -> d.migrate(conn, version, new Version(Start.VERSION))).toList());
|
||||||
|
})
|
||||||
|
.compose(r ->
|
||||||
|
settingRepository.createOrUpdate(SettingKey.version.name(), Start.VERSION))
|
||||||
.onSuccess(r -> {
|
.onSuccess(r -> {
|
||||||
log.info("Database initialized");
|
log.info("Database initialized");
|
||||||
stopPromise.complete();
|
stopPromise.complete();
|
||||||
|
|
|
||||||
|
|
@ -398,12 +398,19 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
ctx.fail(400);
|
ctx.fail(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getTelegramVerticle(telegramId)
|
Optional<TelegramVerticle> telegramVerticleOptional = getTelegramVerticle(telegramId);
|
||||||
.ifPresentOrElse(telegramVerticle ->
|
if (telegramVerticleOptional.isEmpty()) {
|
||||||
telegramVerticle.getDownloadStatistics()
|
ctx.fail(404);
|
||||||
.onSuccess(ctx::json)
|
return;
|
||||||
.onFailure(ctx::fail),
|
}
|
||||||
() -> ctx.fail(404));
|
TelegramVerticle telegramVerticle = telegramVerticleOptional.get();
|
||||||
|
|
||||||
|
String type = ctx.request().getParam("type");
|
||||||
|
String timeRange = ctx.request().getParam("timeRange");
|
||||||
|
(Objects.equals(type, "phase") ? telegramVerticle. getDownloadStatisticsByPhase(Convert.toInt(timeRange, 1)) :
|
||||||
|
telegramVerticle.getDownloadStatistics())
|
||||||
|
.onSuccess(ctx::json)
|
||||||
|
.onFailure(ctx::fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTelegramChange(RoutingContext ctx) {
|
private void handleTelegramChange(RoutingContext ctx) {
|
||||||
|
|
|
||||||
13
api/src/main/java/telegram/files/MessyUtils.java
Normal file
13
api/src/main/java/telegram/files/MessyUtils.java
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package telegram.files;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class MessyUtils {
|
||||||
|
|
||||||
|
public static LocalDateTime withGrouping5Minutes(LocalDateTime time) {
|
||||||
|
int minute = time.getMinute();
|
||||||
|
int minuteGroup = minute / 5;
|
||||||
|
int newMinute = minuteGroup * 5;
|
||||||
|
return time.withMinute(newMinute).withSecond(0).withNano(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import java.util.concurrent.CountDownLatch;
|
||||||
public class Start {
|
public class Start {
|
||||||
private static final Log log = LogFactory.get();
|
private static final Log log = LogFactory.get();
|
||||||
|
|
||||||
public static final String VERSION = "0.1.6";
|
public static final String VERSION = "0.1.7";
|
||||||
|
|
||||||
private static final CountDownLatch shutdownLatch = new CountDownLatch(1);
|
private static final CountDownLatch shutdownLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,9 @@ public class TdApiHelp {
|
||||||
Base64.encode((byte[]) BeanUtil.getProperty(content, "photo.minithumbnail.data")),
|
Base64.encode((byte[]) BeanUtil.getProperty(content, "photo.minithumbnail.data")),
|
||||||
content.caption.text,
|
content.caption.text,
|
||||||
null,
|
null,
|
||||||
"idle"
|
"idle",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -329,7 +331,9 @@ public class TdApiHelp {
|
||||||
Base64.encode((byte[]) BeanUtil.getProperty(content, "video.minithumbnail.data")),
|
Base64.encode((byte[]) BeanUtil.getProperty(content, "video.minithumbnail.data")),
|
||||||
content.caption.text,
|
content.caption.text,
|
||||||
null,
|
null,
|
||||||
"idle"
|
"idle",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +373,9 @@ public class TdApiHelp {
|
||||||
Base64.encode((byte[]) BeanUtil.getProperty(content, "audio.albumCoverMinithumbnail.data")),
|
Base64.encode((byte[]) BeanUtil.getProperty(content, "audio.albumCoverMinithumbnail.data")),
|
||||||
content.caption.text,
|
content.caption.text,
|
||||||
null,
|
null,
|
||||||
"idle"
|
"idle",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -409,7 +415,9 @@ public class TdApiHelp {
|
||||||
Base64.encode((byte[]) BeanUtil.getProperty(content, "document.minithumbnail.data")),
|
Base64.encode((byte[]) BeanUtil.getProperty(content, "document.minithumbnail.data")),
|
||||||
content.caption.text,
|
content.caption.text,
|
||||||
null,
|
null,
|
||||||
"idle"
|
"idle",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ package telegram.files;
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.codec.Base64;
|
import cn.hutool.core.codec.Base64;
|
||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.date.DateField;
|
||||||
|
import cn.hutool.core.date.DatePattern;
|
||||||
|
import cn.hutool.core.date.DateTime;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
|
@ -100,6 +103,7 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated);
|
telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated);
|
||||||
telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived);
|
telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived);
|
||||||
client = Client.create(telegramUpdateHandler, this::handleException, this::handleException);
|
client = Client.create(telegramUpdateHandler, this::handleException, this::handleException);
|
||||||
|
vertx.setPeriodic(60 * 1000, id -> handleSaveAvgSpeed());
|
||||||
this.enableProxy(this.proxyName)
|
this.enableProxy(this.proxyName)
|
||||||
.onSuccess(r -> startPromise.complete())
|
.onSuccess(r -> startPromise.complete())
|
||||||
.onFailure(startPromise::fail);
|
.onFailure(startPromise::fail);
|
||||||
|
|
@ -474,6 +478,72 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Future<JsonObject> getDownloadStatisticsByPhase(Integer timeRange) {
|
||||||
|
// 1: 1 hour, 2: 1 day, 3: 1 week, 4: 1 month
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long startTime = switch (timeRange) {
|
||||||
|
case 1 -> DateUtil.offsetHour(DateUtil.date(), -1).getTime();
|
||||||
|
case 2 -> DateUtil.offsetDay(DateUtil.date(), -1).getTime();
|
||||||
|
case 3 -> DateUtil.offsetWeek(DateUtil.date(), -1).getTime();
|
||||||
|
case 4 -> DateUtil.offsetMonth(DateUtil.date(), -1).getTime();
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + timeRange);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Future.all(
|
||||||
|
DataVerticle.statisticRepository.getRangeStatistics(StatisticRecord.Type.speed, this.telegramRecord.id(), startTime, endTime)
|
||||||
|
.map(statisticRecords -> {
|
||||||
|
TreeMap<String, List<JsonObject>> groupedSpeedStats = new TreeMap<>(Comparator.comparing(
|
||||||
|
switch (timeRange) {
|
||||||
|
case 1, 2 -> (Function<? super String, ? extends DateTime>) time ->
|
||||||
|
DateUtil.parse(time, DatePattern.NORM_DATETIME_MINUTE_FORMAT);
|
||||||
|
case 3, 4 -> DateUtil::parseDate;
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + timeRange);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
for (StatisticRecord record : statisticRecords) {
|
||||||
|
JsonObject data = new JsonObject(record.data());
|
||||||
|
long timestamp = record.timestamp();
|
||||||
|
String time = switch (timeRange) {
|
||||||
|
case 1 ->
|
||||||
|
MessyUtils.withGrouping5Minutes(DateUtil.toLocalDateTime(DateUtil.date(timestamp))).format(DatePattern.NORM_DATETIME_MINUTE_FORMATTER);
|
||||||
|
case 2 ->
|
||||||
|
DateUtil.date(timestamp).setField(DateField.MINUTE, 0).toString(DatePattern.NORM_DATETIME_MINUTE_FORMAT);
|
||||||
|
case 3, 4 ->
|
||||||
|
DateUtil.date(timestamp).setField(DateField.MINUTE, 0).toString(DatePattern.NORM_DATE_FORMAT);
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + timeRange);
|
||||||
|
};
|
||||||
|
groupedSpeedStats.computeIfAbsent(time, k -> new ArrayList<>()).add(data);
|
||||||
|
}
|
||||||
|
return groupedSpeedStats.entrySet().stream()
|
||||||
|
.map(entry -> {
|
||||||
|
JsonObject speedStat = entry.getValue().stream().reduce(new JsonObject()
|
||||||
|
.put("avgSpeed", 0)
|
||||||
|
.put("medianSpeed", 0)
|
||||||
|
.put("maxSpeed", 0)
|
||||||
|
.put("minSpeed", Long.MAX_VALUE),
|
||||||
|
(a, b) -> new JsonObject()
|
||||||
|
.put("avgSpeed", a.getLong("avgSpeed") + b.getLong("avgSpeed"))
|
||||||
|
.put("medianSpeed", a.getLong("medianSpeed") + b.getLong("medianSpeed"))
|
||||||
|
.put("maxSpeed", Math.max(a.getLong("maxSpeed"), b.getLong("maxSpeed")))
|
||||||
|
.put("minSpeed", Math.min(a.getLong("minSpeed"), b.getLong("minSpeed")))
|
||||||
|
);
|
||||||
|
int size = entry.getValue().size();
|
||||||
|
speedStat.put("avgSpeed", speedStat.getLong("avgSpeed") / size)
|
||||||
|
.put("medianSpeed", speedStat.getLong("medianSpeed") / size);
|
||||||
|
return new JsonObject()
|
||||||
|
.put("time", entry.getKey())
|
||||||
|
.put("data", speedStat);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}),
|
||||||
|
DataVerticle.fileRepository.getCompletedRangeStatistics(this.telegramRecord.id(), startTime, endTime, timeRange)
|
||||||
|
)
|
||||||
|
.map(r -> new JsonObject()
|
||||||
|
.put("speedStats", r.resultAt(0))
|
||||||
|
.put("completedStats", r.resultAt(1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public Future<TdApi.Proxy> enableProxy(String proxyName) {
|
public Future<TdApi.Proxy> enableProxy(String proxyName) {
|
||||||
if (StrUtil.isBlank(proxyName)) return Future.succeededFuture();
|
if (StrUtil.isBlank(proxyName)) return Future.succeededFuture();
|
||||||
return DataVerticle.settingRepository.<SettingProxyRecords>getByKey(SettingKey.proxys)
|
return DataVerticle.settingRepository.<SettingProxyRecords>getByKey(SettingKey.proxys)
|
||||||
|
|
@ -625,6 +695,23 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleSaveAvgSpeed() {
|
||||||
|
if (!authorized || telegramRecord == null) return;
|
||||||
|
AvgSpeed.SpeedStats speedStats = avgSpeed.getSpeedStats();
|
||||||
|
if (speedStats.avgSpeed() == 0
|
||||||
|
&& speedStats.minSpeed() == 0
|
||||||
|
&& speedStats.medianSpeed() == 0
|
||||||
|
&& speedStats.maxSpeed() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonObject data = JsonObject.mapFrom(speedStats);
|
||||||
|
data.remove("interval");
|
||||||
|
DataVerticle.statisticRepository.create(new StatisticRecord(Convert.toStr(telegramRecord.id()),
|
||||||
|
StatisticRecord.Type.speed,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
data.encode()));
|
||||||
|
}
|
||||||
|
|
||||||
private void onAuthorizationStateUpdated(TdApi.AuthorizationState authorizationState) {
|
private void onAuthorizationStateUpdated(TdApi.AuthorizationState authorizationState) {
|
||||||
log.debug("[%s] Receive authorization state update: %s".formatted(getRootId(), authorizationState));
|
log.debug("[%s] Receive authorization state update: %s".formatted(getRootId(), authorizationState));
|
||||||
this.lastAuthorizationState = authorizationState;
|
this.lastAuthorizationState = authorizationState;
|
||||||
|
|
@ -695,20 +782,24 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
TdApi.File file = updateFile.file;
|
TdApi.File file = updateFile.file;
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
String localPath = null;
|
String localPath = null;
|
||||||
|
Long completionDate = null;
|
||||||
if (file.local != null && file.local.isDownloadingCompleted) {
|
if (file.local != null && file.local.isDownloadingCompleted) {
|
||||||
localPath = file.local.path;
|
localPath = file.local.path;
|
||||||
|
completionDate = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
FileRecord.DownloadStatus downloadStatus = TdApiHelp.getDownloadStatus(file);
|
FileRecord.DownloadStatus downloadStatus = TdApiHelp.getDownloadStatus(file);
|
||||||
DataVerticle.fileRepository.updateStatus(file.id,
|
DataVerticle.fileRepository.updateStatus(file.id,
|
||||||
file.remote.uniqueId,
|
file.remote.uniqueId,
|
||||||
localPath,
|
localPath,
|
||||||
downloadStatus)
|
downloadStatus,
|
||||||
|
completionDate)
|
||||||
.onSuccess(r -> {
|
.onSuccess(r -> {
|
||||||
if (r == null || r.isEmpty()) return;
|
if (r == null || r.isEmpty()) return;
|
||||||
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject()
|
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject()
|
||||||
.put("fileId", file.id)
|
.put("fileId", file.id)
|
||||||
.put("downloadStatus", r.getString("downloadStatus"))
|
.put("downloadStatus", r.getString("downloadStatus"))
|
||||||
.put("localPath", r.getString("localPath"))
|
.put("localPath", r.getString("localPath"))
|
||||||
|
.put("completionDate", r.getLong("completionDate"))
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
api/src/main/java/telegram/files/repository/Definition.java
Normal file
47
api/src/main/java/telegram/files/repository/Definition.java
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package telegram.files.repository;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Version;
|
||||||
|
import cn.hutool.log.Log;
|
||||||
|
import cn.hutool.log.LogFactory;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.sqlclient.SqlConnection;
|
||||||
|
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public interface Definition {
|
||||||
|
|
||||||
|
Log log = LogFactory.get();
|
||||||
|
|
||||||
|
String getScheme();
|
||||||
|
|
||||||
|
default TreeMap<Version, String[]> getMigrations() {
|
||||||
|
return new TreeMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Future<Void> createTable(SqlConnection conn) {
|
||||||
|
return conn
|
||||||
|
.query(getScheme())
|
||||||
|
.execute()
|
||||||
|
.onFailure(err -> log.error("Failed to create table: %s".formatted(err.getMessage())))
|
||||||
|
.mapEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Future<Void> migrate(SqlConnection conn, Version lastVersion, Version currentVersion) {
|
||||||
|
TreeMap<Version, String[]> migrations = getMigrations();
|
||||||
|
if (migrations.isEmpty()) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
return Future.all(migrations.subMap(lastVersion, false, currentVersion, true).values()
|
||||||
|
.stream()
|
||||||
|
.flatMap(arr -> Stream.of(arr)
|
||||||
|
.map(sql -> conn.query(sql)
|
||||||
|
.execute()
|
||||||
|
.onFailure(e -> log.error("Failed to apply migration: %s".formatted(sql), e)))
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
.onFailure(err -> log.error("Failed to migrate table: %s".formatted(err.getMessage())))
|
||||||
|
.mapEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
package telegram.files.repository;
|
package telegram.files.repository;
|
||||||
|
|
||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.lang.Version;
|
||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.map.MapUtil;
|
||||||
import io.vertx.sqlclient.templates.RowMapper;
|
import io.vertx.sqlclient.templates.RowMapper;
|
||||||
import io.vertx.sqlclient.templates.TupleMapper;
|
import io.vertx.sqlclient.templates.TupleMapper;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
public record FileRecord(int id, //file id will change
|
public record FileRecord(int id, //file id will change
|
||||||
String uniqueId, // unique id of the file, if empty, it means the file is cant be downloaded
|
String uniqueId, // unique id of the file, if empty, it means the file is cant be downloaded
|
||||||
long telegramId,
|
long telegramId,
|
||||||
|
|
@ -20,7 +24,9 @@ public record FileRecord(int id, //file id will change
|
||||||
String thumbnail,
|
String thumbnail,
|
||||||
String caption,
|
String caption,
|
||||||
String localPath,
|
String localPath,
|
||||||
String downloadStatus // 'idle' | 'downloading' | 'paused' | 'completed' | 'error'
|
String downloadStatus, // 'idle' | 'downloading' | 'paused' | 'completed' | 'error'
|
||||||
|
long startDate, // date when the file was started to download
|
||||||
|
Long completionDate // date when the file was downloaded
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public enum DownloadStatus {
|
public enum DownloadStatus {
|
||||||
|
|
@ -46,10 +52,31 @@ public record FileRecord(int id, //file id will change
|
||||||
caption VARCHAR(255),
|
caption VARCHAR(255),
|
||||||
local_path VARCHAR(255),
|
local_path VARCHAR(255),
|
||||||
download_status VARCHAR(255),
|
download_status VARCHAR(255),
|
||||||
|
start_date BIGINT,
|
||||||
|
completion_date BIGINT,
|
||||||
PRIMARY KEY (id, unique_id)
|
PRIMARY KEY (id, unique_id)
|
||||||
)
|
)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static final TreeMap<Version, String[]> MIGRATIONS = new TreeMap<>(MapUtil.ofEntries(
|
||||||
|
MapUtil.entry(new Version("0.1.7"), new String[]{
|
||||||
|
"ALTER TABLE file_record ADD COLUMN start_date BIGINT;",
|
||||||
|
"ALTER TABLE file_record ADD COLUMN completion_date BIGINT;",
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
public static class FileRecordDefinition implements Definition {
|
||||||
|
@Override
|
||||||
|
public String getScheme() {
|
||||||
|
return SCHEME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TreeMap<Version, String[]> getMigrations() {
|
||||||
|
return MIGRATIONS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static RowMapper<FileRecord> ROW_MAPPER = row ->
|
public static RowMapper<FileRecord> ROW_MAPPER = row ->
|
||||||
new FileRecord(row.getInteger("id"),
|
new FileRecord(row.getInteger("id"),
|
||||||
row.getString("unique_id"),
|
row.getString("unique_id"),
|
||||||
|
|
@ -66,7 +93,9 @@ public record FileRecord(int id, //file id will change
|
||||||
row.getString("thumbnail"),
|
row.getString("thumbnail"),
|
||||||
row.getString("caption"),
|
row.getString("caption"),
|
||||||
row.getString("local_path"),
|
row.getString("local_path"),
|
||||||
row.getString("download_status")
|
row.getString("download_status"),
|
||||||
|
Objects.requireNonNullElse(row.getLong("start_date"), 0L),
|
||||||
|
row.getLong("completion_date")
|
||||||
);
|
);
|
||||||
|
|
||||||
public static TupleMapper<FileRecord> PARAM_MAPPER = TupleMapper.mapper(r ->
|
public static TupleMapper<FileRecord> PARAM_MAPPER = TupleMapper.mapper(r ->
|
||||||
|
|
@ -86,11 +115,13 @@ public record FileRecord(int id, //file id will change
|
||||||
MapUtil.entry("thumbnail", r.thumbnail()),
|
MapUtil.entry("thumbnail", r.thumbnail()),
|
||||||
MapUtil.entry("caption", r.caption()),
|
MapUtil.entry("caption", r.caption()),
|
||||||
MapUtil.entry("local_path", r.localPath()),
|
MapUtil.entry("local_path", r.localPath()),
|
||||||
MapUtil.entry("download_status", r.downloadStatus())
|
MapUtil.entry("download_status", r.downloadStatus()),
|
||||||
|
MapUtil.entry("start_date", r.startDate()),
|
||||||
|
MapUtil.entry("completion_date", r.completionDate())
|
||||||
));
|
));
|
||||||
|
|
||||||
public FileRecord withSourceField(int id, long downloadedSize) {
|
public FileRecord withSourceField(int id, long downloadedSize) {
|
||||||
return new FileRecord(id, uniqueId, telegramId, chatId, messageId, date, hasSensitiveContent, size, downloadedSize, type, mimeType, fileName, thumbnail, caption, localPath, downloadStatus);
|
return new FileRecord(id, uniqueId, telegramId, chatId, messageId, date, hasSensitiveContent, size, downloadedSize, type, mimeType, fileName, thumbnail, caption, localPath, downloadStatus, startDate, completionDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package telegram.files.repository;
|
||||||
|
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.MultiMap;
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import org.jooq.lambda.tuple.Tuple3;
|
import org.jooq.lambda.tuple.Tuple3;
|
||||||
|
|
||||||
|
|
@ -9,8 +10,6 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface FileRepository {
|
public interface FileRepository {
|
||||||
Future<Void> init();
|
|
||||||
|
|
||||||
Future<FileRecord> create(FileRecord fileRecord);
|
Future<FileRecord> create(FileRecord fileRecord);
|
||||||
|
|
||||||
Future<Map<Integer, FileRecord>> getFiles(long chatId, List<Integer> fileIds);
|
Future<Map<Integer, FileRecord>> getFiles(long chatId, List<Integer> fileIds);
|
||||||
|
|
@ -25,15 +24,17 @@ public interface FileRepository {
|
||||||
|
|
||||||
Future<JsonObject> getDownloadStatistics(long telegramId);
|
Future<JsonObject> getDownloadStatistics(long telegramId);
|
||||||
|
|
||||||
|
Future<JsonArray> getCompletedRangeStatistics(long id, long startTime, long endTime, int timeRange);
|
||||||
|
|
||||||
Future<Integer> countByStatus(long telegramId, FileRecord.DownloadStatus downloadStatus);
|
Future<Integer> countByStatus(long telegramId, FileRecord.DownloadStatus downloadStatus);
|
||||||
|
|
||||||
Future<JsonObject> updateStatus(int fileId,
|
Future<JsonObject> updateStatus(int fileId,
|
||||||
String uniqueId,
|
String uniqueId,
|
||||||
String localPath,
|
String localPath,
|
||||||
FileRecord.DownloadStatus downloadStatus);
|
FileRecord.DownloadStatus downloadStatus,
|
||||||
|
Long completionDate);
|
||||||
|
|
||||||
Future<Void> updateFileId(int fileId, String uniqueId);
|
Future<Void> updateFileId(int fileId, String uniqueId);
|
||||||
|
|
||||||
Future<Void> deleteByUniqueId(String uniqueId);
|
Future<Void> deleteByUniqueId(String uniqueId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package telegram.files.repository;
|
package telegram.files.repository;
|
||||||
|
|
||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.lang.Version;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public enum SettingKey {
|
public enum SettingKey {
|
||||||
|
version(Version::new),
|
||||||
uniqueOnly(Convert::toBool, false),
|
uniqueOnly(Convert::toBool, false),
|
||||||
needToLoadImages(Convert::toBool, false),
|
needToLoadImages(Convert::toBool, false),
|
||||||
imageLoadSize,
|
imageLoadSize,
|
||||||
|
|
@ -17,6 +19,10 @@ public enum SettingKey {
|
||||||
*/
|
*/
|
||||||
autoDownloadLimit(Convert::toInt),
|
autoDownloadLimit(Convert::toInt),
|
||||||
proxys(value -> new JsonObject(value).mapTo(SettingProxyRecords.class)),
|
proxys(value -> new JsonObject(value).mapTo(SettingProxyRecords.class)),
|
||||||
|
/**
|
||||||
|
* Interval for calculating average speed, in seconds
|
||||||
|
*/
|
||||||
|
avgSpeedInterval(Convert::toInt, 5 * 60),
|
||||||
;
|
;
|
||||||
|
|
||||||
public final Function<String, ?> converter;
|
public final Function<String, ?> converter;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ public record SettingRecord(String key, String value) {
|
||||||
)
|
)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static class SettingRecordDefinition implements Definition {
|
||||||
|
@Override
|
||||||
|
public String getScheme() {
|
||||||
|
return SCHEME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static RowMapper<SettingRecord> ROW_MAPPER = row ->
|
public static RowMapper<SettingRecord> ROW_MAPPER = row ->
|
||||||
new SettingRecord(row.getString("key"),
|
new SettingRecord(row.getString("key"),
|
||||||
row.getString("value")
|
row.getString("value")
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ import io.vertx.core.Future;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface SettingRepository {
|
public interface SettingRepository {
|
||||||
|
|
||||||
Future<Void> init();
|
|
||||||
|
|
||||||
Future<SettingRecord> createOrUpdate(String key, String value);
|
Future<SettingRecord> createOrUpdate(String key, String value);
|
||||||
|
|
||||||
Future<List<SettingRecord>> getByKeys(List<String> keys);
|
Future<List<SettingRecord>> getByKeys(List<String> keys);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package telegram.files.repository;
|
||||||
|
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import io.vertx.sqlclient.templates.RowMapper;
|
||||||
|
import io.vertx.sqlclient.templates.TupleMapper;
|
||||||
|
|
||||||
|
public record StatisticRecord(
|
||||||
|
String relatedId,
|
||||||
|
Type type,
|
||||||
|
long timestamp,
|
||||||
|
String data) {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
speed,
|
||||||
|
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String SCHEME = """
|
||||||
|
CREATE TABLE IF NOT EXISTS statistic_record
|
||||||
|
(
|
||||||
|
related_id VARCHAR(255),
|
||||||
|
type VARCHAR(255),
|
||||||
|
timestamp BIGINT,
|
||||||
|
data TEXT
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static class StatisticRecordDefinition implements Definition {
|
||||||
|
@Override
|
||||||
|
public String getScheme() {
|
||||||
|
return SCHEME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RowMapper<StatisticRecord> ROW_MAPPER = row ->
|
||||||
|
new StatisticRecord(row.getString("related_id"),
|
||||||
|
Type.valueOf(row.getString("type")),
|
||||||
|
row.getLong("timestamp"),
|
||||||
|
row.getString("data")
|
||||||
|
);
|
||||||
|
|
||||||
|
public static TupleMapper<StatisticRecord> PARAM_MAPPER = TupleMapper.mapper(r ->
|
||||||
|
MapUtil.ofEntries(MapUtil.entry("related_id", r.relatedId),
|
||||||
|
MapUtil.entry("type", r.type().name()),
|
||||||
|
MapUtil.entry("timestamp", r.timestamp()),
|
||||||
|
MapUtil.entry("data", r.data())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package telegram.files.repository;
|
||||||
|
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface StatisticRepository {
|
||||||
|
Future<Void> create(StatisticRecord record);
|
||||||
|
|
||||||
|
Future<List<StatisticRecord>> getRangeStatistics(StatisticRecord.Type type,
|
||||||
|
long relatedId,
|
||||||
|
long startTime,
|
||||||
|
long endTime);
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,13 @@ public record TelegramRecord(
|
||||||
)
|
)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
public static class TelegramRecordDefinition implements Definition {
|
||||||
|
@Override
|
||||||
|
public String getScheme() {
|
||||||
|
return SCHEME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static RowMapper<TelegramRecord> ROW_MAPPER = row ->
|
public static RowMapper<TelegramRecord> ROW_MAPPER = row ->
|
||||||
new TelegramRecord(row.getLong("id"),
|
new TelegramRecord(row.getLong("id"),
|
||||||
row.getString("first_name"),
|
row.getString("first_name"),
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ import io.vertx.core.Future;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface TelegramRepository {
|
public interface TelegramRepository {
|
||||||
|
|
||||||
Future<Void> init();
|
|
||||||
|
|
||||||
String getRootPath();
|
String getRootPath();
|
||||||
|
|
||||||
Future<TelegramRecord> create(TelegramRecord telegramRecord);
|
Future<TelegramRecord> create(TelegramRecord telegramRecord);
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,21 @@ package telegram.files.repository.impl;
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.collection.IterUtil;
|
import cn.hutool.core.collection.IterUtil;
|
||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.date.DatePattern;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.map.MapUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.log.Log;
|
import cn.hutool.log.Log;
|
||||||
import cn.hutool.log.LogFactory;
|
import cn.hutool.log.LogFactory;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.MultiMap;
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.jdbcclient.JDBCPool;
|
import io.vertx.jdbcclient.JDBCPool;
|
||||||
import io.vertx.sqlclient.templates.SqlTemplate;
|
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||||
import org.jooq.lambda.tuple.Tuple;
|
import org.jooq.lambda.tuple.Tuple;
|
||||||
import org.jooq.lambda.tuple.Tuple3;
|
import org.jooq.lambda.tuple.Tuple3;
|
||||||
|
import telegram.files.MessyUtils;
|
||||||
import telegram.files.repository.FileRecord;
|
import telegram.files.repository.FileRecord;
|
||||||
import telegram.files.repository.FileRepository;
|
import telegram.files.repository.FileRepository;
|
||||||
|
|
||||||
|
|
@ -34,19 +38,6 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Future<Void> init() {
|
|
||||||
return pool.getConnection()
|
|
||||||
.compose(conn -> conn
|
|
||||||
.query(FileRecord.SCHEME)
|
|
||||||
.execute()
|
|
||||||
.onComplete(r -> conn.close())
|
|
||||||
.onFailure(err -> log.error("Failed to create table file_record: %s".formatted(err.getMessage())))
|
|
||||||
.onSuccess(ps -> log.trace("Successfully created table: file_record"))
|
|
||||||
)
|
|
||||||
.mapEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<FileRecord> create(FileRecord fileRecord) {
|
public Future<FileRecord> create(FileRecord fileRecord) {
|
||||||
return SqlTemplate
|
return SqlTemplate
|
||||||
|
|
@ -54,9 +45,9 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
INSERT INTO file_record(id, unique_id, telegram_id, chat_id, message_id, date, has_sensitive_content, size, downloaded_size,
|
INSERT INTO file_record(id, unique_id, telegram_id, chat_id, message_id, date, has_sensitive_content, size, downloaded_size,
|
||||||
type, mime_type,
|
type, mime_type,
|
||||||
file_name, thumbnail, caption, local_path,
|
file_name, thumbnail, caption, local_path,
|
||||||
download_status)
|
download_status, start_date)
|
||||||
values (#{id}, #{unique_id}, #{telegram_id}, #{chat_id}, #{message_id}, #{date}, #{has_sensitive_content}, #{size}, #{downloaded_size}, #{type},
|
values (#{id}, #{unique_id}, #{telegram_id}, #{chat_id}, #{message_id}, #{date}, #{has_sensitive_content}, #{size}, #{downloaded_size}, #{type},
|
||||||
#{mime_type}, #{file_name}, #{thumbnail}, #{caption}, #{local_path}, #{download_status})
|
#{mime_type}, #{file_name}, #{thumbnail}, #{caption}, #{local_path}, #{download_status}, #{start_date})
|
||||||
""")
|
""")
|
||||||
.mapFrom(FileRecord.PARAM_MAPPER)
|
.mapFrom(FileRecord.PARAM_MAPPER)
|
||||||
.execute(fileRecord)
|
.execute(fileRecord)
|
||||||
|
|
@ -235,6 +226,61 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
.onFailure(err -> log.error("Failed to get download statistics: %s".formatted(err.getMessage())));
|
.onFailure(err -> log.error("Failed to get download statistics: %s".formatted(err.getMessage())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<JsonArray> getCompletedRangeStatistics(long telegramId, long startTime, long endTime, int timeRange) {
|
||||||
|
return SqlTemplate
|
||||||
|
.forQuery(pool, """
|
||||||
|
SELECT strftime(
|
||||||
|
CASE
|
||||||
|
WHEN #{timeRange} = 1 THEN '%Y-%m-%d %H:%M'
|
||||||
|
WHEN #{timeRange} = 2 THEN '%Y-%m-%d %H:00'
|
||||||
|
WHEN #{timeRange} IN (3, 4) THEN '%Y-%m-%d'
|
||||||
|
END,
|
||||||
|
datetime(completion_date / 1000, 'unixepoch')
|
||||||
|
) AS time,
|
||||||
|
COUNT(*) AS total
|
||||||
|
FROM file_record
|
||||||
|
WHERE telegram_id = #{telegramId}
|
||||||
|
AND completion_date IS NOT NULL
|
||||||
|
AND completion_date >= #{startTime}
|
||||||
|
AND completion_date <= #{endTime}
|
||||||
|
GROUP BY time
|
||||||
|
ORDER BY time;
|
||||||
|
""")
|
||||||
|
.mapTo(row -> new JsonObject()
|
||||||
|
.put("time", row.getString("time"))
|
||||||
|
.put("total", row.getInteger("total"))
|
||||||
|
)
|
||||||
|
.execute(Map.of("telegramId", telegramId, "startTime", startTime, "endTime", endTime, "timeRange", timeRange))
|
||||||
|
.map(IterUtil::toList)
|
||||||
|
.map(rs -> {
|
||||||
|
if (CollUtil.isEmpty(rs)) {
|
||||||
|
return JsonArray.of();
|
||||||
|
}
|
||||||
|
if (timeRange == 1) {
|
||||||
|
// Statistics grouped by five minutes
|
||||||
|
return rs.stream()
|
||||||
|
.peek(c -> c.put("time", MessyUtils.withGrouping5Minutes(
|
||||||
|
DateUtil.toLocalDateTime(DateUtil.date(Convert.toLong(c.getString("time"), 0L)))
|
||||||
|
).format(DatePattern.NORM_DATETIME_MINUTE_FORMATTER)))
|
||||||
|
.collect(Collectors.groupingBy(c -> c.getString("time"),
|
||||||
|
Collectors.summingInt(c -> c.getInteger("total"))
|
||||||
|
))
|
||||||
|
.entrySet().stream()
|
||||||
|
.map(e -> new JsonObject()
|
||||||
|
.put("time", e.getKey())
|
||||||
|
.put("total", e.getValue())
|
||||||
|
)
|
||||||
|
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
|
||||||
|
} else {
|
||||||
|
JsonArray jsonArray = new JsonArray();
|
||||||
|
rs.forEach(jsonArray::add);
|
||||||
|
return jsonArray;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFailure(err -> log.error("Failed to get completed statistics: %s".formatted(err.getMessage())));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<Integer> countByStatus(long telegramId, FileRecord.DownloadStatus downloadStatus) {
|
public Future<Integer> countByStatus(long telegramId, FileRecord.DownloadStatus downloadStatus) {
|
||||||
return SqlTemplate
|
return SqlTemplate
|
||||||
|
|
@ -248,7 +294,11 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<JsonObject> updateStatus(int fileId, String uniqueId, String localPath, FileRecord.DownloadStatus downloadStatus) {
|
public Future<JsonObject> updateStatus(int fileId,
|
||||||
|
String uniqueId,
|
||||||
|
String localPath,
|
||||||
|
FileRecord.DownloadStatus downloadStatus,
|
||||||
|
Long completionDate) {
|
||||||
if (StrUtil.isBlank(localPath) && downloadStatus == null) {
|
if (StrUtil.isBlank(localPath) && downloadStatus == null) {
|
||||||
return Future.succeededFuture(null);
|
return Future.succeededFuture(null);
|
||||||
}
|
}
|
||||||
|
|
@ -265,13 +315,16 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
|
|
||||||
return SqlTemplate
|
return SqlTemplate
|
||||||
.forUpdate(pool, """
|
.forUpdate(pool, """
|
||||||
UPDATE file_record SET local_path = #{localPath}, download_status = #{downloadStatus}
|
UPDATE file_record SET local_path = #{localPath},
|
||||||
|
download_status = #{downloadStatus},
|
||||||
|
completion_date = #{completionDate}
|
||||||
WHERE id = #{fileId} AND unique_id = #{uniqueId}
|
WHERE id = #{fileId} AND unique_id = #{uniqueId}
|
||||||
""")
|
""")
|
||||||
.execute(MapUtil.ofEntries(MapUtil.entry("fileId", fileId),
|
.execute(MapUtil.ofEntries(MapUtil.entry("fileId", fileId),
|
||||||
MapUtil.entry("uniqueId", uniqueId),
|
MapUtil.entry("uniqueId", uniqueId),
|
||||||
MapUtil.entry("localPath", pathUpdated ? localPath : record.localPath()),
|
MapUtil.entry("localPath", pathUpdated ? localPath : record.localPath()),
|
||||||
MapUtil.entry("downloadStatus", downloadStatusUpdated ? downloadStatus.name() : record.downloadStatus())
|
MapUtil.entry("downloadStatus", downloadStatusUpdated ? downloadStatus.name() : record.downloadStatus()),
|
||||||
|
MapUtil.entry("completionDate", completionDate)
|
||||||
))
|
))
|
||||||
.onFailure(err ->
|
.onFailure(err ->
|
||||||
log.error("Failed to update file record: %s".formatted(err.getMessage()))
|
log.error("Failed to update file record: %s".formatted(err.getMessage()))
|
||||||
|
|
@ -280,6 +333,7 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
JsonObject result = JsonObject.of();
|
JsonObject result = JsonObject.of();
|
||||||
if (pathUpdated) {
|
if (pathUpdated) {
|
||||||
result.put("localPath", localPath);
|
result.put("localPath", localPath);
|
||||||
|
result.put("completionDate", completionDate);
|
||||||
}
|
}
|
||||||
if (downloadStatusUpdated) {
|
if (downloadStatusUpdated) {
|
||||||
result.put("downloadStatus", downloadStatus.name());
|
result.put("downloadStatus", downloadStatus.name());
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,6 @@ public class SettingRepositoryImpl implements SettingRepository {
|
||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Future<Void> init() {
|
|
||||||
return pool.getConnection()
|
|
||||||
.compose(conn -> conn
|
|
||||||
.query(SettingRecord.SCHEME)
|
|
||||||
.execute()
|
|
||||||
.onComplete(r -> conn.close())
|
|
||||||
.onFailure(err -> log.error("Failed to create table setting_record: %s".formatted(err.getMessage())))
|
|
||||||
.onSuccess(ps -> log.trace("Successfully created table: setting_record"))
|
|
||||||
)
|
|
||||||
.mapEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<SettingRecord> createOrUpdate(String key, String value) {
|
public Future<SettingRecord> createOrUpdate(String key, String value) {
|
||||||
return SqlTemplate
|
return SqlTemplate
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package telegram.files.repository.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.IterUtil;
|
||||||
|
import cn.hutool.log.Log;
|
||||||
|
import cn.hutool.log.LogFactory;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.jdbcclient.JDBCPool;
|
||||||
|
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||||
|
import telegram.files.repository.StatisticRecord;
|
||||||
|
import telegram.files.repository.StatisticRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class StatisticRepositoryImpl implements StatisticRepository {
|
||||||
|
|
||||||
|
private static final Log log = LogFactory.get();
|
||||||
|
|
||||||
|
private final JDBCPool pool;
|
||||||
|
|
||||||
|
public StatisticRepositoryImpl(JDBCPool pool) {
|
||||||
|
this.pool = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<Void> create(StatisticRecord record) {
|
||||||
|
return SqlTemplate
|
||||||
|
.forUpdate(pool, """
|
||||||
|
INSERT INTO statistic_record(related_id, type, timestamp, data)
|
||||||
|
VALUES (#{related_id}, #{type}, #{timestamp}, #{data})
|
||||||
|
""")
|
||||||
|
.mapFrom(StatisticRecord.PARAM_MAPPER)
|
||||||
|
.execute(record)
|
||||||
|
.onSuccess(r -> log.trace("Successfully created statistic record: %s".formatted(record.relatedId())))
|
||||||
|
.onFailure(
|
||||||
|
err -> log.error("Failed to create statistic record: %s".formatted(err.getMessage()))
|
||||||
|
)
|
||||||
|
.mapEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<List<StatisticRecord>> getRangeStatistics(StatisticRecord.Type type,
|
||||||
|
long relatedId,
|
||||||
|
long startTime,
|
||||||
|
long endTime) {
|
||||||
|
return SqlTemplate
|
||||||
|
.forQuery(pool, """
|
||||||
|
SELECT *
|
||||||
|
FROM statistic_record
|
||||||
|
WHERE type = #{type}
|
||||||
|
AND related_id = #{relatedId}
|
||||||
|
AND timestamp >= #{startTime}
|
||||||
|
AND timestamp <= #{endTime}
|
||||||
|
ORDER BY timestamp
|
||||||
|
""")
|
||||||
|
.mapTo(StatisticRecord.ROW_MAPPER)
|
||||||
|
.execute(Map.of(
|
||||||
|
"type", type.name(),
|
||||||
|
"relatedId", relatedId,
|
||||||
|
"startTime", startTime,
|
||||||
|
"endTime", endTime
|
||||||
|
))
|
||||||
|
.map(IterUtil::toList)
|
||||||
|
.onFailure(
|
||||||
|
err -> log.error("Failed to get range statistics: %s".formatted(err.getMessage()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,19 +26,6 @@ public class TelegramRepositoryImpl implements TelegramRepository {
|
||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Future<Void> init() {
|
|
||||||
return pool.getConnection()
|
|
||||||
.compose(conn -> conn
|
|
||||||
.query(TelegramRecord.SCHEME)
|
|
||||||
.execute()
|
|
||||||
.onComplete(r -> conn.close())
|
|
||||||
.onFailure(err -> log.error("Failed to create table telegram_record: %s".formatted(err.getMessage())))
|
|
||||||
.onSuccess(ps -> log.trace("Successfully created table: telegram_record"))
|
|
||||||
)
|
|
||||||
.mapEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getRootPath() {
|
public String getRootPath() {
|
||||||
return Config.TELEGRAM_ROOT + File.separator + UUID.randomUUID();
|
return Config.TELEGRAM_ROOT + File.separator + UUID.randomUUID();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package telegram.files;
|
package telegram.files;
|
||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.lang.Version;
|
||||||
import cn.hutool.log.Log;
|
import cn.hutool.log.Log;
|
||||||
import cn.hutool.log.LogFactory;
|
import cn.hutool.log.LogFactory;
|
||||||
import io.vertx.core.Vertx;
|
import io.vertx.core.Vertx;
|
||||||
|
|
@ -9,6 +10,7 @@ import io.vertx.junit5.VertxTestContext;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import telegram.files.repository.FileRecord;
|
import telegram.files.repository.FileRecord;
|
||||||
|
import telegram.files.repository.SettingKey;
|
||||||
import telegram.files.repository.TelegramRecord;
|
import telegram.files.repository.TelegramRecord;
|
||||||
|
|
||||||
@ExtendWith(VertxExtension.class)
|
@ExtendWith(VertxExtension.class)
|
||||||
|
|
@ -22,8 +24,8 @@ public class DataVerticleTest {
|
||||||
log.debug("DATA_PATH:" + DataVerticle.getDataPath());
|
log.debug("DATA_PATH:" + DataVerticle.getDataPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterEach
|
||||||
static void tearDown() {
|
void tearDown() {
|
||||||
String dataPath = DataVerticle.getDataPath();
|
String dataPath = DataVerticle.getDataPath();
|
||||||
if (FileUtil.file(dataPath).exists()) {
|
if (FileUtil.file(dataPath).exists()) {
|
||||||
FileUtil.del(dataPath);
|
FileUtil.del(dataPath);
|
||||||
|
|
@ -37,6 +39,17 @@ public class DataVerticleTest {
|
||||||
.onComplete(testContext.succeedingThenComplete());
|
.onComplete(testContext.succeedingThenComplete());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Check table initialization")
|
||||||
|
void checkTableInitialization(Vertx vertx, VertxTestContext testContext) {
|
||||||
|
DataVerticle.settingRepository.<Version>getByKey(SettingKey.version)
|
||||||
|
.onComplete(testContext.succeeding(r -> testContext.verify(() -> {
|
||||||
|
Assertions.assertNotNull(r);
|
||||||
|
Assertions.assertEquals(new Version(Start.VERSION), r);
|
||||||
|
testContext.completeNow();
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Test Create telegram record")
|
@DisplayName("Test Create telegram record")
|
||||||
void createTelegramRecordTest(Vertx vertx, VertxTestContext testContext) {
|
void createTelegramRecordTest(Vertx vertx, VertxTestContext testContext) {
|
||||||
|
|
@ -69,7 +82,7 @@ public class DataVerticleTest {
|
||||||
@DisplayName("Test Get file record by primary key")
|
@DisplayName("Test Get file record by primary key")
|
||||||
void getFileRecordByPrimaryKeyTest(Vertx vertx, VertxTestContext testContext) {
|
void getFileRecordByPrimaryKeyTest(Vertx vertx, VertxTestContext testContext) {
|
||||||
FileRecord fileRecord = new FileRecord(
|
FileRecord fileRecord = new FileRecord(
|
||||||
1, "unique_id", 1, 1, 1, 1, false, 1, 0, "type", "mime_type", "file_name", "thumbnail", "caption", "local_path", "download_status"
|
1, "unique_id", 1, 1, 1, 1, false, 1, 0, "type", "mime_type", "file_name", "thumbnail", "caption", "local_path", "download_status", 0, null
|
||||||
);
|
);
|
||||||
DataVerticle.fileRepository.create(fileRecord)
|
DataVerticle.fileRepository.create(fileRecord)
|
||||||
.compose(r -> DataVerticle.fileRepository.getByPrimaryKey(r.id(), r.uniqueId()))
|
.compose(r -> DataVerticle.fileRepository.getByPrimaryKey(r.id(), r.uniqueId()))
|
||||||
|
|
@ -83,15 +96,17 @@ public class DataVerticleTest {
|
||||||
@DisplayName("Test update file status")
|
@DisplayName("Test update file status")
|
||||||
void updateFileStatusTest(Vertx vertx, VertxTestContext testContext) {
|
void updateFileStatusTest(Vertx vertx, VertxTestContext testContext) {
|
||||||
FileRecord fileRecord = new FileRecord(
|
FileRecord fileRecord = new FileRecord(
|
||||||
1, "unique_id", 1, 1, 1, 1, false, 1, 0, "type", "mime_type", "file_name", "thumbnail", "caption", null, "download_status"
|
1, "unique_id", 1, 1, 1, 1, false, 1, 0, "type", "mime_type", "file_name", "thumbnail", "caption", null, "download_status", 0, null
|
||||||
);
|
);
|
||||||
String updateLocalPath = "local_path";
|
String updateLocalPath = "local_path";
|
||||||
|
Long completionDate = 1L;
|
||||||
DataVerticle.fileRepository.create(fileRecord)
|
DataVerticle.fileRepository.create(fileRecord)
|
||||||
.compose(r -> DataVerticle.fileRepository.updateStatus(r.id(), r.uniqueId(), updateLocalPath, FileRecord.DownloadStatus.downloading))
|
.compose(r -> DataVerticle.fileRepository.updateStatus(r.id(), r.uniqueId(), updateLocalPath, FileRecord.DownloadStatus.downloading, completionDate))
|
||||||
.compose(r -> {
|
.compose(r -> {
|
||||||
testContext.verify(() -> {
|
testContext.verify(() -> {
|
||||||
Assertions.assertEquals(updateLocalPath, r.getString("localPath"));
|
Assertions.assertEquals(updateLocalPath, r.getString("localPath"));
|
||||||
Assertions.assertEquals(FileRecord.DownloadStatus.downloading.name(), r.getString("downloadStatus"));
|
Assertions.assertEquals(FileRecord.DownloadStatus.downloading.name(), r.getString("downloadStatus"));
|
||||||
|
Assertions.assertEquals(completionDate, r.getLong("completionDate"));
|
||||||
});
|
});
|
||||||
return DataVerticle.fileRepository.getByPrimaryKey(fileRecord.id(), fileRecord.uniqueId());
|
return DataVerticle.fileRepository.getByPrimaryKey(fileRecord.id(), fileRecord.uniqueId());
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ GET http://{{server}}//file/preview?chatId={{chatId}}&messageId={{messageId}}
|
||||||
Cookie: {{tf}}
|
Cookie: {{tf}}
|
||||||
|
|
||||||
### Get download statistics
|
### Get download statistics
|
||||||
GET http://{{server}}/telegram/{{telegramId}}/download-statistics
|
GET http://{{server}}/telegram/{{telegramId}}/download-statistics?type=phase&timeRange=3
|
||||||
Cookie: {{tf}}
|
Cookie: {{tf}}
|
||||||
|
|
||||||
### Telegram GetMe
|
### Telegram GetMe
|
||||||
|
|
|
||||||
590
web/package-lock.json
generated
590
web/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "telegram-files-web",
|
"name": "telegram-files-web",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "telegram-files-web",
|
"name": "telegram-files-web",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.2.0",
|
"@dnd-kit/core": "^6.2.0",
|
||||||
"@dnd-kit/modifiers": "^8.0.0",
|
"@dnd-kit/modifiers": "^8.0.0",
|
||||||
|
|
@ -35,12 +35,14 @@
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
"motion": "^11.15.0",
|
||||||
"next": "^15.0.1",
|
"next": "^15.0.1",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-use-websocket": "^4.11.1",
|
"react-use-websocket": "^4.11.1",
|
||||||
|
"recharts": "^2.15.0",
|
||||||
"spoiled": "^0.3.2",
|
"spoiled": "^0.3.2",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|
@ -2160,6 +2162,60 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.56.12",
|
"version": "8.56.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
||||||
|
|
@ -3364,8 +3420,117 @@
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
"devOptional": true
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
|
|
@ -3450,6 +3615,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
|
|
@ -3526,6 +3696,15 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
|
@ -4210,12 +4389,25 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-3VpaQYf+CDFdRQfgsb+3vY7XaKjM35WCMoQTTE8h4S/eUkHzyJFOOA/gATYgoLejy4FBrEQD/sXe5Auk4cW/AQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
|
|
@ -4752,6 +4944,14 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
||||||
|
|
@ -5313,6 +5513,11 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
|
@ -5411,6 +5616,31 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion": {
|
||||||
|
"version": "11.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion/-/motion-11.15.0.tgz",
|
||||||
|
"integrity": "sha512-iZ7dwADQJWGsqsSkBhNHdI2LyYWU+hA1Nhy357wCLZq1yHxGImgt3l7Yv0HT/WOskcYDq9nxdedyl4zUv7UFFw==",
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^11.15.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/motion-dom": {
|
"node_modules/motion-dom": {
|
||||||
"version": "11.14.3",
|
"version": "11.14.3",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||||
|
|
@ -6045,7 +6275,6 @@
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
|
@ -6121,8 +6350,7 @@
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
|
|
@ -6169,6 +6397,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-smooth": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-equals": "^5.0.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
|
@ -6190,6 +6432,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-use-websocket": {
|
"node_modules/react-use-websocket": {
|
||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz",
|
||||||
|
|
@ -6214,6 +6471,41 @@
|
||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"eventemitter3": "^4.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-is": "^18.3.1",
|
||||||
|
"react-smooth": "^4.0.0",
|
||||||
|
"recharts-scale": "^0.4.4",
|
||||||
|
"tiny-invariant": "^1.3.1",
|
||||||
|
"victory-vendor": "^36.6.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts-scale": {
|
||||||
|
"version": "0.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||||
|
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||||
|
"dependencies": {
|
||||||
|
"decimal.js-light": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz",
|
||||||
|
|
@ -6982,6 +7274,11 @@
|
||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|
@ -7245,6 +7542,27 @@
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "36.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
@ -8597,6 +8915,60 @@
|
||||||
"@t3-oss/env-core": "0.10.1"
|
"@t3-oss/env-core": "0.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/d3-array": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
|
||||||
|
},
|
||||||
|
"@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
|
||||||
|
},
|
||||||
|
"@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
|
||||||
|
},
|
||||||
|
"@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
|
||||||
|
},
|
||||||
|
"@types/d3-scale": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
|
||||||
|
},
|
||||||
|
"@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
|
||||||
|
},
|
||||||
"@types/eslint": {
|
"@types/eslint": {
|
||||||
"version": "8.56.12",
|
"version": "8.56.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
||||||
|
|
@ -9353,8 +9725,84 @@
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
"devOptional": true
|
},
|
||||||
|
"d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"requires": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
|
||||||
|
},
|
||||||
|
"d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
|
||||||
|
},
|
||||||
|
"d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
|
||||||
|
},
|
||||||
|
"d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"requires": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="
|
||||||
|
},
|
||||||
|
"d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"requires": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"requires": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"requires": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"requires": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="
|
||||||
},
|
},
|
||||||
"damerau-levenshtein": {
|
"damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
|
|
@ -9409,6 +9857,11 @@
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
|
||||||
|
},
|
||||||
"deep-is": {
|
"deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
|
|
@ -9467,6 +9920,15 @@
|
||||||
"esutils": "^2.0.2"
|
"esutils": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eastasianwidth": {
|
"eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
|
@ -9989,12 +10451,22 @@
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fast-equals": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-3VpaQYf+CDFdRQfgsb+3vY7XaKjM35WCMoQTTE8h4S/eUkHzyJFOOA/gATYgoLejy4FBrEQD/sXe5Auk4cW/AQ=="
|
||||||
|
},
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
|
|
@ -10376,6 +10848,11 @@
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
|
||||||
|
},
|
||||||
"is-array-buffer": {
|
"is-array-buffer": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
||||||
|
|
@ -10758,6 +11235,11 @@
|
||||||
"p-locate": "^5.0.0"
|
"p-locate": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"lodash.merge": {
|
"lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
|
@ -10829,6 +11311,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
||||||
},
|
},
|
||||||
|
"motion": {
|
||||||
|
"version": "11.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion/-/motion-11.15.0.tgz",
|
||||||
|
"integrity": "sha512-iZ7dwADQJWGsqsSkBhNHdI2LyYWU+hA1Nhy357wCLZq1yHxGImgt3l7Yv0HT/WOskcYDq9nxdedyl4zUv7UFFw==",
|
||||||
|
"requires": {
|
||||||
|
"framer-motion": "^11.15.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"motion-dom": {
|
"motion-dom": {
|
||||||
"version": "11.14.3",
|
"version": "11.14.3",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||||
|
|
@ -11175,7 +11666,6 @@
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
|
@ -11219,8 +11709,7 @@
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"react-remove-scroll": {
|
"react-remove-scroll": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
|
|
@ -11243,6 +11732,16 @@
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-smooth": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||||
|
"requires": {
|
||||||
|
"fast-equals": "^5.0.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-style-singleton": {
|
"react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
|
@ -11252,6 +11751,17 @@
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-use-websocket": {
|
"react-use-websocket": {
|
||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz",
|
||||||
|
|
@ -11273,6 +11783,36 @@
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"recharts": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
|
||||||
|
"requires": {
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"eventemitter3": "^4.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-is": "^18.3.1",
|
||||||
|
"react-smooth": "^4.0.0",
|
||||||
|
"recharts-scale": "^0.4.4",
|
||||||
|
"tiny-invariant": "^1.3.1",
|
||||||
|
"victory-vendor": "^36.6.8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recharts-scale": {
|
||||||
|
"version": "0.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||||
|
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||||
|
"requires": {
|
||||||
|
"decimal.js-light": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"reflect.getprototypeof": {
|
"reflect.getprototypeof": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz",
|
||||||
|
|
@ -11790,6 +12330,11 @@
|
||||||
"thenify": ">= 3.1.0 < 4"
|
"thenify": ">= 3.1.0 < 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
|
||||||
|
},
|
||||||
"to-regex-range": {
|
"to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|
@ -11970,6 +12515,27 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.1"
|
"@radix-ui/react-dialog": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"victory-vendor": {
|
||||||
|
"version": "36.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"which": {
|
"which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,14 @@
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
"motion": "^11.15.0",
|
||||||
"next": "^15.0.1",
|
"next": "^15.0.1",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-use-websocket": "^4.11.1",
|
"react-use-websocket": "^4.11.1",
|
||||||
|
"recharts": "^2.15.0",
|
||||||
"spoiled": "^0.3.2",
|
"spoiled": "^0.3.2",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { type TelegramFile } from "@/lib/types";
|
import { type TelegramFile } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
|
Clock10,
|
||||||
|
ClockArrowDown,
|
||||||
Download,
|
Download,
|
||||||
FileAudio2Icon,
|
FileAudio2Icon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
|
|
@ -183,7 +185,7 @@ function FileDrawer({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
<span>Date</span>
|
<span>Received At</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{formatDistanceToNow(new Date(file.date * 1000), {
|
{formatDistanceToNow(new Date(file.date * 1000), {
|
||||||
|
|
@ -201,6 +203,34 @@ function FileDrawer({
|
||||||
{prettyBytes(file.downloadedSize)}
|
{prettyBytes(file.downloadedSize)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(file.downloadStatus !== 'idle' && file.startDate !== 0) && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Clock10 className="h-4 w-4" />
|
||||||
|
<span>Download At</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{formatDistanceToNow(new Date(file.startDate), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{file.completionDate && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<ClockArrowDown className="h-4 w-4" />
|
||||||
|
<span>Completion At</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{formatDistanceToNow(new Date(file.completionDate), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Captions, Clock, Copy, FileCheck } from "lucide-react";
|
import { Captions, Clock, ClockArrowDown, Copy, FileCheck } from "lucide-react";
|
||||||
import SpoiledWrapper from "@/components/spoiled-wrapper";
|
import SpoiledWrapper from "@/components/spoiled-wrapper";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
|
@ -63,9 +63,9 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<p className="flex items-center gap-2">
|
<p className="flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-4 w-4" />
|
||||||
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
|
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
|
||||||
|
|
@ -81,6 +81,25 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{file.completionDate && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<p className="flex items-center gap-2">
|
||||||
|
<ClockArrowDown className="h-4 w-4" />
|
||||||
|
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
|
||||||
|
{formatDistanceToNow(new Date(file.completionDate), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="max-w-80 text-wrap rounded p-2">
|
||||||
|
{`File downloaded at ${new Date(file.completionDate).toLocaleString()}`}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
249
web/src/components/file-phase-statistics.tsx
Normal file
249
web/src/components/file-phase-statistics.tsx
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
import { request } from "@/lib/api";
|
||||||
|
import prettyBytes from "pretty-bytes"; // Type definitions
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
type TimeRange = "1" | "2" | "3" | "4";
|
||||||
|
|
||||||
|
interface SpeedData {
|
||||||
|
avgSpeed: number;
|
||||||
|
medianSpeed: number;
|
||||||
|
maxSpeed: number;
|
||||||
|
minSpeed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpeedStats {
|
||||||
|
time: string;
|
||||||
|
data: SpeedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompletedStats {
|
||||||
|
time: string;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
speedStats: SpeedStats[];
|
||||||
|
completedStats: CompletedStats[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string, timeRange: TimeRange): string => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
|
||||||
|
switch (timeRange) {
|
||||||
|
case "1": // Last hour
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
case "2": // Last day
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
case "3": // Last week
|
||||||
|
case "4": // Last month
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TelegramStatsProps {
|
||||||
|
telegramId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeRangeOptions = [
|
||||||
|
{ value: "1", label: "1 Hour" },
|
||||||
|
{ value: "2", label: "24 Hours" },
|
||||||
|
{ value: "3", label: "1 Week" },
|
||||||
|
{ value: "4", label: "30 Days" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const axisStyle = {
|
||||||
|
fontSize: 11,
|
||||||
|
fill: "#6b7280", // text-gray-500
|
||||||
|
};
|
||||||
|
|
||||||
|
const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => {
|
||||||
|
const [timeRange, setTimeRange] = useState<TimeRange>("2");
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR<ApiResponse, Error>(
|
||||||
|
`/telegram/${telegramId}/download-statistics?type=phase&timeRange=${timeRange}`,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="p-4 text-red-500">Failed to load statistics</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || !data) {
|
||||||
|
return <div className="p-4 text-gray-500">Loading statistics...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform speed data for the chart
|
||||||
|
const speedChartData = data.speedStats.map((stat) => ({
|
||||||
|
time: formatDate(stat.time, timeRange),
|
||||||
|
"Average Speed": stat.data.avgSpeed,
|
||||||
|
"Median Speed": stat.data.medianSpeed,
|
||||||
|
"Max Speed": stat.data.maxSpeed,
|
||||||
|
"Min Speed": stat.data.minSpeed,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Transform completion data for the chart
|
||||||
|
const completionChartData = data.completedStats.map((stat) => ({
|
||||||
|
time: formatDate(stat.time, timeRange),
|
||||||
|
"Completed Downloads": stat.total,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative space-y-6">
|
||||||
|
<div className="absolute -top-14 right-1">
|
||||||
|
<Select
|
||||||
|
value={timeRange}
|
||||||
|
onValueChange={(value: TimeRange) => setTimeRange(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue placeholder="Select time range" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{timeRangeOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="px-1">Download Speed Over Time</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-1">
|
||||||
|
<div className="h-80">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<AreaChart data={speedChartData}>
|
||||||
|
<CartesianGrid stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tick={axisStyle}
|
||||||
|
interval="preserveStartEnd"
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tickFormatter={(value: number) =>
|
||||||
|
prettyBytes(value, { bits: true })
|
||||||
|
}
|
||||||
|
tick={axisStyle}
|
||||||
|
interval="preserveStartEnd"
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) =>
|
||||||
|
prettyBytes(value, { bits: true })
|
||||||
|
}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "6px",
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend wrapperStyle={axisStyle} iconType="circle" />
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="Max Speed"
|
||||||
|
stroke="#06b6d4"
|
||||||
|
fill="#06b6d4"
|
||||||
|
fillOpacity={0.6}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="Average Speed"
|
||||||
|
stroke="#8b5cf6"
|
||||||
|
fill="#8b5cf6"
|
||||||
|
fillOpacity={0.8}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="Median Speed"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
fill="#f59e0b"
|
||||||
|
fillOpacity={0.2}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="Min Speed"
|
||||||
|
stroke="#ec4899"
|
||||||
|
fill="#ec4899"
|
||||||
|
fillOpacity={0.7}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="px-1">Completed Downloads Over Time</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-1">
|
||||||
|
<div className="h-80">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={completionChartData}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<YAxis tick={axisStyle} interval="preserveStartEnd" />
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ fill: "rgba(59, 130, 246, 0.1)" }}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "6px",
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend wrapperStyle={axisStyle} iconType="rect" />
|
||||||
|
<Bar
|
||||||
|
dataKey="Completed Downloads"
|
||||||
|
fill="#299d90"
|
||||||
|
fillOpacity={0.8}
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TelegramStats;
|
||||||
|
|
@ -14,6 +14,9 @@ import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
||||||
import Proxys from "@/components/proxys";
|
import Proxys from "@/components/proxys";
|
||||||
import SettingsForm from "@/components/settings-form";
|
import SettingsForm from "@/components/settings-form";
|
||||||
import About from "@/components/about";
|
import About from "@/components/about";
|
||||||
|
import { ChartColumnIncreasingIcon } from "@/components/ui/chart-column-increasing";
|
||||||
|
import { LayoutPanelTopIcon } from "@/components/ui/layout-panel-top";
|
||||||
|
import FilePhaseStatistics from "@/components/file-phase-statistics";
|
||||||
|
|
||||||
export const SettingsDialog: React.FC = () => {
|
export const SettingsDialog: React.FC = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -53,9 +56,24 @@ export const SettingsDialog: React.FC = () => {
|
||||||
<SettingsForm />
|
<SettingsForm />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="statistics" className="h-full overflow-hidden">
|
<TabsContent value="statistics" className="h-full overflow-hidden">
|
||||||
<div className="h-full flex flex-col overflow-y-scroll">
|
<div className="flex h-full flex-col overflow-y-scroll">
|
||||||
{accountId ? (
|
{accountId ? (
|
||||||
<FileStatistics telegramId={accountId} />
|
<Tabs defaultValue="panel">
|
||||||
|
<TabsList className="bg-none!">
|
||||||
|
<TabsTrigger value="panel" className="w-10 h-10 scale-75 data-[state=active]:bg-accent">
|
||||||
|
<LayoutPanelTopIcon />
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="phase" className="w-10 h-10 scale-75 data-[state=active]:bg-accent">
|
||||||
|
<ChartColumnIncreasingIcon />
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="panel">
|
||||||
|
<FileStatistics telegramId={accountId} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="phase">
|
||||||
|
<FilePhaseStatistics telegramId={accountId} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
|
|
|
||||||
79
web/src/components/ui/chart-column-increasing.tsx
Normal file
79
web/src/components/ui/chart-column-increasing.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useAnimation, type Variants } from "motion/react";
|
||||||
|
|
||||||
|
const frameVariants: Variants = {
|
||||||
|
visible: { opacity: 1 },
|
||||||
|
hidden: { opacity: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineVariants: Variants = {
|
||||||
|
visible: { pathLength: 1, opacity: 1 },
|
||||||
|
hidden: { pathLength: 0, opacity: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartColumnIncreasingIcon = () => {
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
const handleHoverStart = async () => {
|
||||||
|
await controls.start((i) => ({
|
||||||
|
pathLength: 0,
|
||||||
|
opacity: 0,
|
||||||
|
transition: { delay: i * 0.1, duration: 0.3 },
|
||||||
|
}));
|
||||||
|
await controls.start((i) => ({
|
||||||
|
pathLength: 1,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { delay: i * 0.1, duration: 0.3 },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHoverEnd = () => {
|
||||||
|
void controls.start("visible");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer select-none items-center justify-center rounded-md p-2 transition-colors duration-200 hover:bg-accent"
|
||||||
|
onMouseEnter={handleHoverStart}
|
||||||
|
onMouseLeave={handleHoverEnd}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<motion.path
|
||||||
|
variants={lineVariants}
|
||||||
|
initial="visible"
|
||||||
|
animate={controls}
|
||||||
|
custom={1}
|
||||||
|
d="M13 17V9"
|
||||||
|
/>
|
||||||
|
<motion.path
|
||||||
|
variants={lineVariants}
|
||||||
|
initial="visible"
|
||||||
|
animate={controls}
|
||||||
|
custom={2}
|
||||||
|
d="M18 17V5"
|
||||||
|
/>
|
||||||
|
<motion.path variants={frameVariants} d="M3 3v16a2 2 0 0 0 2 2h16" />
|
||||||
|
<motion.path
|
||||||
|
variants={lineVariants}
|
||||||
|
initial="visible"
|
||||||
|
animate={controls}
|
||||||
|
custom={0}
|
||||||
|
d="M8 17v-3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ChartColumnIncreasingIcon };
|
||||||
365
web/src/components/ui/chart.tsx
Normal file
365
web/src/components/ui/chart.tsx
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
"use client";
|
||||||
|
/* eslint-disable */
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: React.ReactNode;
|
||||||
|
icon?: React.ComponentType;
|
||||||
|
} & (
|
||||||
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChartContextProps = {
|
||||||
|
config: ChartConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
|
function useChart() {
|
||||||
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChart must be used within a <ChartContainer />");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartContainer = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
config: ChartConfig;
|
||||||
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"];
|
||||||
|
}
|
||||||
|
>(({ id, className, children, config, ...props }, ref) => {
|
||||||
|
const uniqueId = React.useId();
|
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContext.Provider value={{ config }}>
|
||||||
|
<div
|
||||||
|
data-chart={chartId}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChartStyle id={chartId} config={config} />
|
||||||
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
|
{children}
|
||||||
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</ChartContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ChartContainer.displayName = "Chart";
|
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([, config]) => config.theme || config.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!colorConfig.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color =
|
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
|
itemConfig.color;
|
||||||
|
return color ? ` --color-${key}: ${color};` : null;
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
|
const ChartTooltipContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
hideLabel?: boolean;
|
||||||
|
hideIndicator?: boolean;
|
||||||
|
indicator?: "line" | "dot" | "dashed";
|
||||||
|
nameKey?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
className,
|
||||||
|
indicator = "dot",
|
||||||
|
hideLabel = false,
|
||||||
|
hideIndicator = false,
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
labelClassName,
|
||||||
|
formatter,
|
||||||
|
color,
|
||||||
|
nameKey,
|
||||||
|
labelKey,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => {
|
||||||
|
if (hideLabel || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = payload;
|
||||||
|
const key = `${labelKey || item!.dataKey || item!.name || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label;
|
||||||
|
|
||||||
|
if (labelFormatter) {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||||
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
|
indicator === "dot" && "items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||||
|
{item.value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ChartTooltipContent.displayName = "ChartTooltip";
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
|
const ChartLegendContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> &
|
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
|
hideIcon?: boolean;
|
||||||
|
nameKey?: string;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
|
if (!payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{itemConfig?.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ChartLegendContent.displayName = "ChartLegend";
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: unknown,
|
||||||
|
key: string,
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in payload &&
|
||||||
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config];
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
};
|
||||||
92
web/src/components/ui/layout-panel-top.tsx
Normal file
92
web/src/components/ui/layout-panel-top.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useAnimation } from "motion/react";
|
||||||
|
|
||||||
|
const LayoutPanelTopIcon = () => {
|
||||||
|
const controls = useAnimation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer select-none items-center justify-center rounded-md p-2 transition-colors duration-200 hover:bg-accent"
|
||||||
|
onMouseEnter={() => controls.start("animate")}
|
||||||
|
onMouseLeave={() => controls.start("normal")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<motion.rect
|
||||||
|
width="18"
|
||||||
|
height="7"
|
||||||
|
x="3"
|
||||||
|
y="3"
|
||||||
|
rx="1"
|
||||||
|
initial="normal"
|
||||||
|
animate={controls}
|
||||||
|
variants={{
|
||||||
|
normal: { opacity: 1, translateY: 0 },
|
||||||
|
animate: {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [-5, 0],
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: 0.5, times: [0.2, 1] },
|
||||||
|
duration: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.rect
|
||||||
|
width="7"
|
||||||
|
height="7"
|
||||||
|
x="3"
|
||||||
|
y="14"
|
||||||
|
rx="1"
|
||||||
|
initial="normal"
|
||||||
|
animate={controls}
|
||||||
|
variants={{
|
||||||
|
normal: { opacity: 1, translateX: 0 },
|
||||||
|
animate: {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateX: [-10, 0],
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: 0.7, times: [0.5, 1] },
|
||||||
|
translateX: { delay: 0.3 },
|
||||||
|
duration: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.rect
|
||||||
|
width="7"
|
||||||
|
height="7"
|
||||||
|
x="14"
|
||||||
|
y="14"
|
||||||
|
rx="1"
|
||||||
|
initial="normal"
|
||||||
|
animate={controls}
|
||||||
|
variants={{
|
||||||
|
normal: { opacity: 1, translateX: 0 },
|
||||||
|
animate: {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateX: [10, 0],
|
||||||
|
transition: {
|
||||||
|
opacity: { duration: 0.8, times: [0.5, 1] },
|
||||||
|
translateX: { delay: 0.4 },
|
||||||
|
duration: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LayoutPanelTopIcon };
|
||||||
|
|
@ -30,6 +30,7 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
{
|
{
|
||||||
downloadStatus: DownloadStatus;
|
downloadStatus: DownloadStatus;
|
||||||
localPath: string;
|
localPath: string;
|
||||||
|
completionDate: number;
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
>({});
|
>({});
|
||||||
|
|
@ -74,6 +75,7 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
fileId: number;
|
fileId: number;
|
||||||
downloadStatus: DownloadStatus;
|
downloadStatus: DownloadStatus;
|
||||||
localPath: string;
|
localPath: string;
|
||||||
|
completionDate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
setLatestFileStatus((prev) => ({
|
setLatestFileStatus((prev) => ({
|
||||||
|
|
@ -82,6 +84,7 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
downloadStatus:
|
downloadStatus:
|
||||||
data.downloadStatus ?? prev[data.fileId]?.downloadStatus,
|
data.downloadStatus ?? prev[data.fileId]?.downloadStatus,
|
||||||
localPath: data.localPath ?? prev[data.fileId]?.localPath,
|
localPath: data.localPath ?? prev[data.fileId]?.localPath,
|
||||||
|
completionDate: data.completionDate ?? prev[data.fileId]?.completionDate,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}, [lastJsonMessage]);
|
}, [lastJsonMessage]);
|
||||||
|
|
@ -96,6 +99,8 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
downloadStatus:
|
downloadStatus:
|
||||||
latestFilesStatus[file.id]?.downloadStatus ?? file.downloadStatus,
|
latestFilesStatus[file.id]?.downloadStatus ?? file.downloadStatus,
|
||||||
localPath: latestFilesStatus[file.id]?.localPath ?? file.localPath,
|
localPath: latestFilesStatus[file.id]?.localPath ?? file.localPath,
|
||||||
|
completionDate:
|
||||||
|
latestFilesStatus[file.id]?.completionDate ?? file.completionDate,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ export type TelegramFile = {
|
||||||
caption: string;
|
caption: string;
|
||||||
localPath: string;
|
localPath: string;
|
||||||
hasSensitiveContent: boolean;
|
hasSensitiveContent: boolean;
|
||||||
|
startDate: number;
|
||||||
|
completionDate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDFile = {
|
export type TDFile = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue