✨ feat: Support using proxies
This commit is contained in:
parent
8086e8a485
commit
d4ddc70639
17 changed files with 882 additions and 191 deletions
|
|
@ -127,6 +127,8 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
router.get("/telegram/:telegramId/chat/:chatId/files/count").handler(this::handleTelegramFilesCount);
|
||||
router.get("/telegram/:telegramId/download-statistics").handler(this::handleTelegramDownloadStatistics);
|
||||
router.post("/telegrams/change").handler(this::handleTelegramChange);
|
||||
router.post("/telegram/:telegramId/toggle-proxy").handler(this::handleTelegramToggleProxy);
|
||||
router.get("/telegram/:telegramId/ping").handler(this::handleTelegramPing);
|
||||
|
||||
router.get("/file/preview").handler(this::handleFilePreview);
|
||||
router.post("/file/start-download").handler(this::handleFileStartDownload);
|
||||
|
|
@ -273,9 +275,12 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
);
|
||||
return;
|
||||
}
|
||||
JsonObject jsonObject = ctx.body().asJsonObject();
|
||||
String proxyName = jsonObject.getString("proxyName");
|
||||
|
||||
TelegramVerticle newTelegramVerticle = new TelegramVerticle(DataVerticle.telegramRepository.getRootPath());
|
||||
newTelegramVerticle.bindHttpSession(sessionId);
|
||||
newTelegramVerticle.setProxy(proxyName);
|
||||
sessionTelegramVerticles.put(sessionId, newTelegramVerticle);
|
||||
telegramVerticles.add(newTelegramVerticle);
|
||||
vertx.deployVerticle(newTelegramVerticle)
|
||||
|
|
@ -401,6 +406,29 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
}, () -> ctx.fail(404));
|
||||
}
|
||||
|
||||
private void handleTelegramToggleProxy(RoutingContext ctx) {
|
||||
String telegramId = ctx.request().getParam("telegramId");
|
||||
getTelegramVerticle(telegramId)
|
||||
.ifPresentOrElse(telegramVerticle ->
|
||||
telegramVerticle.toggleProxy(ctx.body().asJsonObject())
|
||||
.onSuccess(r -> ctx.json(JsonObject.of("proxy", r)))
|
||||
.onFailure(ctx::fail), () -> ctx.fail(404));
|
||||
}
|
||||
|
||||
private void handleTelegramPing(RoutingContext ctx) {
|
||||
String telegramId = ctx.pathParam("telegramId");
|
||||
if (StrUtil.isBlank(telegramId)) {
|
||||
ctx.fail(400);
|
||||
return;
|
||||
}
|
||||
getTelegramVerticle(telegramId)
|
||||
.ifPresentOrElse(telegramVerticle ->
|
||||
telegramVerticle.ping()
|
||||
.onSuccess(r -> ctx.json(JsonObject.of("ping", r)))
|
||||
.onFailure(ctx::fail), () -> ctx.fail(404)
|
||||
);
|
||||
}
|
||||
|
||||
private void handleTelegramApi(RoutingContext ctx) {
|
||||
String method = ctx.pathParam("method");
|
||||
if (method == null) {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@ import cn.hutool.core.util.StrUtil;
|
|||
import cn.hutool.core.util.TypeUtil;
|
||||
import cn.hutool.log.Log;
|
||||
import cn.hutool.log.LogFactory;
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.CompositeFuture;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.*;
|
||||
import io.vertx.core.impl.NoStackTraceException;
|
||||
import io.vertx.core.json.Json;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
|
|
@ -24,10 +21,7 @@ import org.drinkless.tdlib.Client;
|
|||
import org.drinkless.tdlib.TdApi;
|
||||
import org.jooq.lambda.tuple.Tuple;
|
||||
import org.jooq.lambda.tuple.Tuple2;
|
||||
import telegram.files.repository.FileRecord;
|
||||
import telegram.files.repository.SettingAutoRecords;
|
||||
import telegram.files.repository.SettingKey;
|
||||
import telegram.files.repository.TelegramRecord;
|
||||
import telegram.files.repository.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOError;
|
||||
|
|
@ -50,6 +44,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
|
||||
public String rootPath;
|
||||
|
||||
private String proxyName;
|
||||
|
||||
private String rootId;
|
||||
|
||||
private boolean needDelete = false;
|
||||
|
|
@ -74,6 +70,7 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
public TelegramVerticle(TelegramRecord telegramRecord) {
|
||||
this.telegramRecord = telegramRecord;
|
||||
this.rootPath = telegramRecord.rootPath();
|
||||
this.proxyName = telegramRecord.proxy();
|
||||
}
|
||||
|
||||
public String getRootId() {
|
||||
|
|
@ -87,14 +84,21 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
return telegramRecord == null ? this.getRootId() : telegramRecord.id();
|
||||
}
|
||||
|
||||
public void setProxy(String proxyName) {
|
||||
this.proxyName = proxyName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
public void start(Promise<Void> startPromise) {
|
||||
TelegramUpdateHandler telegramUpdateHandler = new TelegramUpdateHandler();
|
||||
telegramUpdateHandler.setOnAuthorizationStateUpdated(this::onAuthorizationStateUpdated);
|
||||
telegramUpdateHandler.setOnFileUpdated(this::onFileUpdated);
|
||||
telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated);
|
||||
telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived);
|
||||
client = Client.create(telegramUpdateHandler, this::handleException, this::handleException);
|
||||
this.enableProxy(this.proxyName)
|
||||
.onSuccess(r -> startPromise.complete())
|
||||
.onFailure(startPromise::fail);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
|
|
@ -129,7 +133,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
.put("status", "inactive")
|
||||
.put("rootPath", this.rootPath)
|
||||
.put("isPremium", false)
|
||||
.put("lastAuthorizationState", lastAuthorizationState);
|
||||
.put("lastAuthorizationState", lastAuthorizationState)
|
||||
.put("proxy", this.proxyName);
|
||||
if (this.telegramRecord != null) {
|
||||
jsonObject.put("id", Convert.toStr(this.telegramRecord.id()))
|
||||
.put("name", this.telegramRecord.firstName());
|
||||
|
|
@ -146,7 +151,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(user, "profilePhoto.minithumbnail.data")))
|
||||
.put("status", "active")
|
||||
.put("rootPath", this.rootPath)
|
||||
.put("isPremium", user.isPremium);
|
||||
.put("isPremium", user.isPremium)
|
||||
.put("proxy", this.proxyName);
|
||||
promise.complete(result);
|
||||
})
|
||||
.onFailure(e -> {
|
||||
|
|
@ -158,6 +164,9 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
|
||||
public Future<JsonArray> getChats(Long activatedChatId, String query) {
|
||||
Consumer<TdApi.Chats> insertActivatedChatId = chats -> {
|
||||
if (chats == null || ArrayUtil.isEmpty(chats.chatIds)) {
|
||||
return;
|
||||
}
|
||||
long[] chatIds = chats.chatIds;
|
||||
if (activatedChatId != null && !ArrayUtil.contains(chatIds, activatedChatId)) {
|
||||
chatIds = (long[]) ArrayUtil.insert(chatIds, 0, activatedChatId);
|
||||
|
|
@ -293,7 +302,9 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
if (fileRecord != null) {
|
||||
promise.complete();
|
||||
} else {
|
||||
DataVerticle.fileRepository.create(TdApiHelp.getFileHandler(message).get().convertFileRecord(telegramRecord.id()))
|
||||
DataVerticle.fileRepository.create(TdApiHelp.getFileHandler(message)
|
||||
.orElseThrow()
|
||||
.convertFileRecord(telegramRecord.id()))
|
||||
.onSuccess(r -> promise.complete())
|
||||
.onFailure(promise::fail);
|
||||
}
|
||||
|
|
@ -402,6 +413,87 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
return DataVerticle.fileRepository.getDownloadStatistics(this.telegramRecord.id());
|
||||
}
|
||||
|
||||
public Future<TdApi.Proxy> enableProxy(String proxyName) {
|
||||
if (StrUtil.isBlank(proxyName)) return Future.succeededFuture();
|
||||
return DataVerticle.settingRepository.<SettingProxyRecords>getByKey(SettingKey.proxys)
|
||||
.map(settingProxyRecords -> Optional.ofNullable(settingProxyRecords)
|
||||
.flatMap(r -> r.getProxy(proxyName))
|
||||
.orElseThrow(() -> new NoStackTraceException("Proxy %s not found".formatted(proxyName)))
|
||||
)
|
||||
.compose(proxy -> this.getTdProxy(proxy)
|
||||
.map(r -> Tuple.tuple(proxy, r))
|
||||
)
|
||||
.compose(tuple -> {
|
||||
SettingProxyRecords.Item proxy = tuple.v1;
|
||||
TdApi.Proxy tdProxy = tuple.v2;
|
||||
boolean edit = false;
|
||||
if (tdProxy != null) {
|
||||
if (tdProxy.isEnabled) {
|
||||
return Future.succeededFuture(tdProxy);
|
||||
}
|
||||
edit = true;
|
||||
}
|
||||
|
||||
TdApi.ProxyType proxyType;
|
||||
if (Objects.equals(proxy.type, "http")) {
|
||||
proxyType = new TdApi.ProxyTypeHttp(proxy.username, proxy.password, false);
|
||||
} else if (Objects.equals(proxy.type, "socks5")) {
|
||||
proxyType = new TdApi.ProxyTypeSocks5(proxy.username, proxy.password);
|
||||
} else {
|
||||
return Future.failedFuture("Unsupported proxy type: %s".formatted(proxy.type));
|
||||
}
|
||||
return edit ? this.execute(new TdApi.EditProxy(tdProxy.id, proxy.server, proxy.port, true, proxyType))
|
||||
: this.execute(new TdApi.AddProxy(proxy.server, proxy.port, true, proxyType));
|
||||
})
|
||||
.compose(r -> DataVerticle.telegramRepository.update(this.telegramRecord.withProxy(proxyName)).map(r))
|
||||
.compose(r -> {
|
||||
this.proxyName = proxyName;
|
||||
return Future.succeededFuture(r);
|
||||
});
|
||||
}
|
||||
|
||||
public Future<TdApi.Proxy> toggleProxy(JsonObject jsonObject) {
|
||||
String toggleProxyName = jsonObject.getString("proxyName");
|
||||
if (Objects.equals(toggleProxyName, this.proxyName)) {
|
||||
return Future.succeededFuture();
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(toggleProxyName) && StrUtil.isNotBlank(this.proxyName)) {
|
||||
// disable proxy
|
||||
return this.execute(new TdApi.DisableProxy())
|
||||
.compose(r -> DataVerticle.telegramRepository.update(this.telegramRecord.withProxy(null)))
|
||||
.andThen(r -> {
|
||||
this.proxyName = null;
|
||||
this.telegramRecord = r.result();
|
||||
})
|
||||
.mapEmpty();
|
||||
} else {
|
||||
return this.enableProxy(toggleProxyName);
|
||||
}
|
||||
}
|
||||
|
||||
public Future<TdApi.Proxy> getTdProxy(SettingProxyRecords.Item proxy) {
|
||||
return this.execute(new TdApi.GetProxies())
|
||||
.map(proxies -> Stream.of(proxies.proxies)
|
||||
.filter(proxy::equalsTdProxy)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
}
|
||||
|
||||
public Future<TdApi.Proxy> getTdProxy() {
|
||||
return this.execute(new TdApi.GetProxies())
|
||||
.map(proxies -> Stream.of(proxies.proxies)
|
||||
.filter(p -> p.isEnabled)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
}
|
||||
|
||||
public Future<Double> ping() {
|
||||
return this.getTdProxy()
|
||||
.compose(proxy -> this.execute(new TdApi.PingProxy(proxy == null ? 0 : proxy.id)))
|
||||
.map(r -> r.seconds);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <R extends TdApi.Object> Future<R> execute(TdApi.Function<R> method) {
|
||||
log.trace("[%s] Execute method: %s".formatted(getRootId(), TypeUtil.getTypeArgument(method.getClass())));
|
||||
|
|
@ -506,7 +598,7 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
if (telegramRecord == null) {
|
||||
this.execute(new TdApi.GetMe())
|
||||
.compose(user ->
|
||||
DataVerticle.telegramRepository.create(new TelegramRecord(user.id, user.firstName, this.rootPath))
|
||||
DataVerticle.telegramRepository.create(new TelegramRecord(user.id, user.firstName, this.rootPath, this.proxyName))
|
||||
)
|
||||
.compose(record -> {
|
||||
telegramRecord = record;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public enum SettingKey {
|
|||
* Auto download limit for each telegram account
|
||||
*/
|
||||
autoDownloadLimit(Convert::toInt),
|
||||
proxys(value -> new JsonObject(value).mapTo(SettingProxyRecords.class)),
|
||||
;
|
||||
|
||||
public final Function<String, ?> converter;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package telegram.files.repository;
|
||||
|
||||
import org.drinkless.tdlib.TdApi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SettingProxyRecords {
|
||||
|
||||
public List<Item> items;
|
||||
|
||||
public static class Item {
|
||||
/**
|
||||
* should be unique
|
||||
*/
|
||||
public String name;
|
||||
|
||||
public String server;
|
||||
|
||||
public int port;
|
||||
|
||||
public String username;
|
||||
|
||||
public String password;
|
||||
|
||||
/**
|
||||
* http, socks5
|
||||
*/
|
||||
public String type;
|
||||
|
||||
public boolean equalsTdProxy(TdApi.Proxy proxy) {
|
||||
return Objects.equals(name, proxy.server)
|
||||
&& port == proxy.port
|
||||
&& (Objects.equals(type, "http") ? proxy.type instanceof TdApi.ProxyTypeHttp : proxy.type instanceof TdApi.ProxyTypeSocks5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public SettingProxyRecords() {
|
||||
this.items = new ArrayList<>();
|
||||
}
|
||||
|
||||
public Optional<Item> getProxy(String name) {
|
||||
return items.stream().filter(item -> Objects.equals(name, item.name)).findFirst();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,28 +4,38 @@ import cn.hutool.core.map.MapUtil;
|
|||
import io.vertx.sqlclient.templates.RowMapper;
|
||||
import io.vertx.sqlclient.templates.TupleMapper;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record TelegramRecord(long id, String firstName, String rootPath) {
|
||||
public record TelegramRecord(
|
||||
long id,
|
||||
String firstName,
|
||||
String rootPath,
|
||||
String proxy
|
||||
) {
|
||||
|
||||
public static final String SCHEME = """
|
||||
CREATE TABLE IF NOT EXISTS telegram_record
|
||||
(
|
||||
id BIGINT PRIMARY KEY,
|
||||
first_name VARCHAR(255),
|
||||
root_path VARCHAR(255)
|
||||
root_path VARCHAR(255),
|
||||
proxy VARCHAR(255)
|
||||
)
|
||||
""";
|
||||
|
||||
public static RowMapper<TelegramRecord> ROW_MAPPER = row ->
|
||||
new TelegramRecord(row.getLong("id"),
|
||||
row.getString("first_name"),
|
||||
row.getString("root_path")
|
||||
row.getString("root_path"),
|
||||
row.getString("proxy")
|
||||
);
|
||||
|
||||
public static TupleMapper<TelegramRecord> PARAM_MAPPER = TupleMapper.mapper(r ->
|
||||
Map.ofEntries(MapUtil.entry("id", r.id),
|
||||
Map.entry("first_name", r.firstName()),
|
||||
Map.entry("root_path", r.rootPath())
|
||||
MapUtil.ofEntries(MapUtil.entry("id", r.id),
|
||||
MapUtil.entry("first_name", r.firstName()),
|
||||
MapUtil.entry("root_path", r.rootPath()),
|
||||
MapUtil.entry("proxy", r.proxy())
|
||||
));
|
||||
|
||||
public TelegramRecord withProxy(String proxy) {
|
||||
return new TelegramRecord(id, firstName, rootPath, proxy);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ public interface TelegramRepository {
|
|||
|
||||
Future<TelegramRecord> create(TelegramRecord telegramRecord);
|
||||
|
||||
Future<TelegramRecord> update(TelegramRecord telegramRecord);
|
||||
|
||||
Future<TelegramRecord> getById(long id);
|
||||
|
||||
Future<List<TelegramRecord>> getAll();
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ public class TelegramRepositoryImpl implements TelegramRepository {
|
|||
@Override
|
||||
public Future<TelegramRecord> create(TelegramRecord telegramRecord) {
|
||||
return SqlTemplate
|
||||
.forUpdate(pool, "INSERT INTO telegram_record(id, first_name, root_path) VALUES (#{id}, #{first_name}, #{root_path})")
|
||||
.forUpdate(pool, "INSERT INTO telegram_record(id, first_name, root_path, proxy) VALUES (#{id}, #{first_name}, #{root_path}, #{proxy})")
|
||||
.mapFrom(TelegramRecord.PARAM_MAPPER)
|
||||
.execute(telegramRecord)
|
||||
.map(r -> telegramRecord)
|
||||
|
|
@ -57,6 +57,19 @@ public class TelegramRepositoryImpl implements TelegramRepository {
|
|||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<TelegramRecord> update(TelegramRecord telegramRecord) {
|
||||
return SqlTemplate
|
||||
.forUpdate(pool, "UPDATE telegram_record SET first_name = #{first_name}, root_path = #{root_path}, proxy = #{proxy} WHERE id = #{id}")
|
||||
.mapFrom(TelegramRecord.PARAM_MAPPER)
|
||||
.execute(telegramRecord)
|
||||
.map(r -> telegramRecord)
|
||||
.onSuccess(r -> log.debug("Successfully updated telegram record: %s".formatted(telegramRecord.id())))
|
||||
.onFailure(
|
||||
err -> log.error("Failed to update telegram record: %s".formatted(err.getMessage()))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<TelegramRecord> getById(long id) {
|
||||
return SqlTemplate
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ Cookie: {{tf}}
|
|||
POST http://{{server}}/telegram/create
|
||||
Cookie: {{tf}}
|
||||
|
||||
### Ping
|
||||
GET http://{{server}}/telegram/{{telegramId}}/ping
|
||||
Cookie: {{tf}}
|
||||
|
||||
### Get chats
|
||||
GET http://{{server}}/telegram/{{telegramId}}/chats
|
||||
Cookie: {{tf}}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ import {
|
|||
} from "@/lib/websocket-types";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { POST, telegramApi, type TelegramApiArg } from "@/lib/api";
|
||||
import { request, telegramApi, type TelegramApiArg } from "@/lib/api";
|
||||
import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type TelegramApiResult } from "@/lib/types";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
|
||||
import ProxysDialog from "@/components/proxys-dialog";
|
||||
|
||||
export function AccountDialog({
|
||||
children,
|
||||
|
|
@ -37,12 +38,24 @@ export function AccountDialog({
|
|||
}) {
|
||||
const { toast } = useToast();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [proxyName, setProxyName] = useState<string | undefined>();
|
||||
const {
|
||||
data: createData,
|
||||
trigger: triggerCreate,
|
||||
isMutating: isCreateMutating,
|
||||
error: createError,
|
||||
} = useSWRMutation<{ id: string }, Error>("/telegram/create", POST);
|
||||
} = useSWRMutation<{ id: string }, Error>(
|
||||
"/telegram/create",
|
||||
async (key: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return await request(key, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
proxyName: proxyName,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
const { trigger: triggerMethod, isMutating: isMethodMutating } =
|
||||
useSWRMutation<TelegramApiResult, Error, string, TelegramApiArg>(
|
||||
"/telegram/api",
|
||||
|
|
@ -137,7 +150,6 @@ export function AccountDialog({
|
|||
if (isMethodMutating) return true;
|
||||
const lastCode = methodCodes[methodCodes.length - 1];
|
||||
if (!lastCode) return false;
|
||||
console.log(lastCode, methodCompleteCodes);
|
||||
return !methodCompleteCodes.includes(lastCode);
|
||||
}, [isMethodMutating, methodCodes, methodCompleteCodes]);
|
||||
|
||||
|
|
@ -173,7 +185,7 @@ export function AccountDialog({
|
|||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent
|
||||
aria-describedby={undefined}
|
||||
className="h-full w-full md:h-auto md:min-h-40 md:min-w-[550px]"
|
||||
className="relative h-full w-full pb-10 md:h-auto md:min-h-40 md:min-w-[550px]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
|
|
@ -184,7 +196,7 @@ export function AccountDialog({
|
|||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{!debounceIsCreateMutating && !initSuccessfully && (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<Button
|
||||
className={cn(
|
||||
"w-full",
|
||||
|
|
@ -291,6 +303,13 @@ export function AccountDialog({
|
|||
)}
|
||||
</form>
|
||||
)}
|
||||
<ProxysDialog
|
||||
telegramId={createData?.id}
|
||||
proxyName={proxyName}
|
||||
onProxyNameChange={setProxyName}
|
||||
enableSelect={true}
|
||||
className="absolute bottom-1 right-1"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { AccountDialog } from "@/components/account-dialog";
|
|||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BorderBeam } from "@/components/ui/border-beam";
|
||||
import ProxysDialog from "@/components/proxys-dialog";
|
||||
|
||||
interface EmptyStateProps {
|
||||
hasAccounts: boolean;
|
||||
|
|
@ -55,15 +56,19 @@ export function EmptyState({
|
|||
</p>
|
||||
</>
|
||||
)}
|
||||
<AccountDialog isAdd={true}>
|
||||
<div className="relative rounded-md">
|
||||
<BorderBeam size={60} duration={12} delay={9} />
|
||||
<Button variant="outline">
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Add Account
|
||||
</Button>
|
||||
</div>
|
||||
</AccountDialog>
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<AccountDialog isAdd={true}>
|
||||
<div className="relative rounded-md">
|
||||
<BorderBeam size={60} duration={12} delay={9} />
|
||||
<Button variant="outline">
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Add Account
|
||||
</Button>
|
||||
</div>
|
||||
</AccountDialog>
|
||||
|
||||
<ProxysDialog />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasAccounts && accounts.length > 0 && onSelectAccount && (
|
||||
|
|
|
|||
49
web/src/components/proxy-ping.tsx
Normal file
49
web/src/components/proxy-ping.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import useSWR from "swr";
|
||||
import { ChevronsLeftRightEllipsis } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export default function ProxyPing({ accountId }: { accountId: string }) {
|
||||
const { data, isLoading, error, mutate } = useSWR<
|
||||
{
|
||||
ping: number;
|
||||
},
|
||||
Error
|
||||
>(`/telegram/${accountId}/ping`, {
|
||||
errorRetryCount: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
className="flex items-center space-x-2 rounded bg-gray-100 p-1"
|
||||
onClick={() => {
|
||||
if (isLoading) return;
|
||||
void mutate(undefined, true);
|
||||
}}
|
||||
>
|
||||
<ChevronsLeftRightEllipsis className="h-4 w-4 text-gray-500" />
|
||||
{isLoading && (
|
||||
<div className="h-6 w-24 animate-pulse rounded bg-gray-200"></div>
|
||||
)}
|
||||
{!isLoading && error && <span>Connection error</span>}
|
||||
{!isLoading && data && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>Ping: {(data.ping * 1000).toFixed(0)}ms</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Current latency for accessing telegram API
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
48
web/src/components/proxys-dialog.tsx
Normal file
48
web/src/components/proxys-dialog.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EarthLock } from "lucide-react";
|
||||
import Proxys, { type ProxysProps } from "@/components/proxys";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ProxysDialogProps = {
|
||||
className?: string;
|
||||
} & ProxysProps;
|
||||
|
||||
export default function ProxysDialog({
|
||||
className,
|
||||
...proxyProps
|
||||
}: ProxysDialogProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-auto rounded-full bg-primary px-2 text-accent hover:bg-primary/90 hover:text-accent",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<EarthLock className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="md:max-w-2/3 h-full w-full max-w-full md:h-3/4 md:w-2/3"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Proxys</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<div className="mt-3">
|
||||
<Proxys {...proxyProps} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
337
web/src/components/proxys.tsx
Normal file
337
web/src/components/proxys.tsx
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { useSettings } from "@/hooks/use-settings";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Edit2, Plus, Trash2 } from "lucide-react";
|
||||
import { type Proxy } from "@/lib/types";
|
||||
import { BorderBeam } from "@/components/ui/border-beam";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { request } from "@/lib/api";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import ProxyPing from "@/components/proxy-ping";
|
||||
|
||||
export interface ProxysProps {
|
||||
enableSelect?: boolean;
|
||||
telegramId?: string;
|
||||
proxyName?: string;
|
||||
onProxyNameChange?: (name: string) => void;
|
||||
}
|
||||
|
||||
export default function Proxys({
|
||||
enableSelect,
|
||||
telegramId,
|
||||
proxyName,
|
||||
onProxyNameChange,
|
||||
}: ProxysProps) {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const [innerProxyName, setInnerProxyName] = useState("");
|
||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingProxy, setEditingProxy] = useState<Proxy | null>(null);
|
||||
const [formState, setFormState] = useState<Proxy>({
|
||||
name: "",
|
||||
server: "",
|
||||
port: 0,
|
||||
username: "",
|
||||
password: "",
|
||||
type: "http",
|
||||
});
|
||||
const { trigger: triggerProxy, isMutating: isToggleProxyMutating } =
|
||||
useSWRMutation<{ id: string }, Error>(
|
||||
telegramId ? `/telegram/${telegramId}/toggle-proxy` : undefined,
|
||||
async (key: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return await request(key, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
proxyName: innerProxyName,
|
||||
}),
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: innerProxyName
|
||||
? `Proxy is set to ${innerProxyName}`
|
||||
: "Proxy is disabled",
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Parse proxy settings
|
||||
const proxys = (
|
||||
(settings?.proxys
|
||||
? JSON.parse(settings.proxys)
|
||||
: {
|
||||
items: [],
|
||||
}) as {
|
||||
items: Proxy[];
|
||||
}
|
||||
).items;
|
||||
|
||||
// Open dialog for adding or editing proxy
|
||||
const handleOpenDialog = (proxy: Proxy | null = null): void => {
|
||||
setEditingProxy(proxy);
|
||||
setFormState(
|
||||
proxy ?? {
|
||||
name: "",
|
||||
server: "",
|
||||
port: 0,
|
||||
username: "",
|
||||
password: "",
|
||||
type: "http",
|
||||
},
|
||||
);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// Close dialog
|
||||
const handleCloseDialog = (): void => {
|
||||
setDialogOpen(false);
|
||||
setEditingProxy(null);
|
||||
};
|
||||
|
||||
// Delete proxy with confirmation
|
||||
const handleDeleteProxy = (proxy: Proxy): void => {
|
||||
if (confirm(`Are you sure you want to delete the proxy ${proxy.name}?`)) {
|
||||
const updatedProxys = proxys.filter((p) => p.name !== proxy.name);
|
||||
void updateSettings({
|
||||
proxys: JSON.stringify({
|
||||
items: updatedProxys,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Save proxy (either add or edit)
|
||||
const handleSaveProxy = (): void => {
|
||||
const updatedProxys = editingProxy
|
||||
? proxys.map((p) => (p.name === editingProxy.name ? formState : p))
|
||||
: [...proxys, formState];
|
||||
void updateSettings({ proxys: JSON.stringify({ items: updatedProxys }) }); // Submit to server
|
||||
handleCloseDialog();
|
||||
};
|
||||
|
||||
// Handle form input change
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
): void => {
|
||||
const { name, value } = e.target;
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
[name]: name === "port" ? parseInt(value, 10) : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleProxySubmit = async () => {
|
||||
if (telegramId) {
|
||||
await triggerProxy();
|
||||
} else {
|
||||
onProxyNameChange?.(innerProxyName);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-2xl font-bold">Proxys</h1>
|
||||
{telegramId && <ProxyPing accountId={telegramId} />}
|
||||
</div>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<Plus className="mr-2 h-5 w-5" /> Add Proxy
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{proxys.map((proxy) => (
|
||||
<Card
|
||||
key={proxy.name}
|
||||
className={cn("relative hover:shadow-lg", {
|
||||
"cursor-pointer": enableSelect,
|
||||
"border border-primary":
|
||||
enableSelect && proxy.name === innerProxyName,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (innerProxyName === proxy.name) {
|
||||
setInnerProxyName("");
|
||||
} else {
|
||||
setInnerProxyName(proxy.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{proxyName && proxy.name === proxyName && (
|
||||
<BorderBeam size={100} duration={12} delay={9} />
|
||||
)}
|
||||
<CardHeader className="p-2 px-3">
|
||||
<CardTitle className="text-xl font-semibold">
|
||||
{proxy.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 px-3">
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<p className="text-lg text-gray-700">
|
||||
{proxy.type.toUpperCase()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">{`${proxy.server}:${proxy.port}`}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end space-x-2 p-1">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => handleOpenDialog(proxy)}
|
||||
>
|
||||
<Edit2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteProxy(proxy)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-red-600" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{enableSelect && (
|
||||
<Button
|
||||
className="absolute bottom-4 right-4"
|
||||
disabled={isToggleProxyMutating}
|
||||
onClick={() => handleProxySubmit()}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Dialog open={isDialogOpen} onOpenChange={handleCloseDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingProxy ? "Edit Proxy" : "Add Proxy"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{/* Radio buttons for proxy type */}
|
||||
<div>
|
||||
<label className="mb-1 block text-lg text-gray-700">Type</label>
|
||||
<div className="flex space-x-4">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
value="http"
|
||||
checked={formState.type === "http"}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>HTTP</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
value="socks5"
|
||||
checked={formState.type === "socks5"}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<span>SOCKS5</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Other input fields */}
|
||||
<div>
|
||||
<label className="mb-1 block text-lg text-gray-700">Name</label>
|
||||
<Input
|
||||
name="name"
|
||||
value={formState.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter proxy name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-lg text-gray-700">
|
||||
Proxy server address and port number
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-500">
|
||||
Server
|
||||
</label>
|
||||
<Input
|
||||
name="server"
|
||||
value={formState.server}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter server address"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-sm font-medium text-gray-500">
|
||||
Port
|
||||
</label>
|
||||
<Input
|
||||
name="port"
|
||||
type="number"
|
||||
value={formState.port}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter port number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-1 text-lg text-gray-700">
|
||||
Authentication (optional)
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500">
|
||||
Username
|
||||
</label>
|
||||
<Input
|
||||
name="username"
|
||||
value={formState.username}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
value={formState.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleCloseDialog} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveProxy}>
|
||||
{editingProxy ? "Save" : "Add"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { type FormEvent, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -6,45 +6,17 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Bell, Settings } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Settings } from "lucide-react";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useSettings } from "@/hooks/use-settings";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import FileStatistics from "@/components/file-statistics";
|
||||
import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
||||
import Proxys from "@/components/proxys";
|
||||
import SettingsForm from "@/components/settings-form";
|
||||
|
||||
export const SettingsDialog: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { accountId } = useTelegramAccount();
|
||||
|
||||
const { settings, setSetting, updateSettings } = useSettings();
|
||||
|
||||
const imageLoadSizeOptions = [
|
||||
{ value: "s", label: "box 100x100" },
|
||||
{ value: "m", label: "box 320x320" },
|
||||
{ value: "x", label: "box 800x800" },
|
||||
{ value: "y", label: "box 1280x1280" },
|
||||
{ value: "w", label: "box 2560x2560" },
|
||||
{ value: "a", label: "crop 160x160" },
|
||||
{ value: "b", label: "crop 320x320" },
|
||||
{ value: "c", label: "crop 640x640" },
|
||||
{ value: "d", label: "crop 1280x1280" },
|
||||
];
|
||||
|
||||
const handleSave = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
await updateSettings();
|
||||
};
|
||||
const { account, accountId } = useTelegramAccount();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
|
|
@ -73,128 +45,34 @@ export const SettingsDialog: React.FC = () => {
|
|||
<TabsList className="justify-start">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="statistics">Statistics</TabsTrigger>
|
||||
<TabsTrigger value="proxys">Proxys</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="general"
|
||||
className="flex flex-col overflow-hidden"
|
||||
>
|
||||
<form onSubmit={handleSave} className="flex flex-col space-y-4 overflow-y-scroll">
|
||||
<div className="flex flex-col space-y-4 overflow-y-scroll">
|
||||
<p className="rounded-md bg-gray-50 p-2 text-sm text-muted-foreground shadow">
|
||||
<Bell className="mr-2 inline-block h-4 w-4" />
|
||||
These settings will be applied to all accounts.
|
||||
</p>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="unique-only">Unique Only</Label>
|
||||
<Checkbox
|
||||
id="unique-only"
|
||||
checked={settings?.uniqueOnly === "true"}
|
||||
onCheckedChange={(checked) =>
|
||||
void setSetting("uniqueOnly", String(checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Show only unique file in the table. If disabled, will show
|
||||
all. <br />
|
||||
<strong>Warning:</strong> If enabled, the number of
|
||||
documents on the form will be inaccurate.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="need-load-preview-images">
|
||||
Need Load Preview Images
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="need-load-preview-images"
|
||||
checked={settings?.needToLoadImages === "true"}
|
||||
onCheckedChange={(checked) => {
|
||||
void setSetting("needToLoadImages", String(checked));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{settings?.needToLoadImages === "true" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="image-load-size">Load Size</Label>
|
||||
<Select
|
||||
value={settings.imageLoadSize}
|
||||
onValueChange={(v) =>
|
||||
void setSetting("imageLoadSize", v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="image-load-size">
|
||||
<SelectValue placeholder="Select Image Load Size" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{imageLoadSizeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The size of the image to load in the browser.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="show-sensitive-content">
|
||||
Show Sensitive Content
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="show-sensitive-content"
|
||||
checked={settings?.showSensitiveContent === "true"}
|
||||
onCheckedChange={(checked) =>
|
||||
void setSetting("showSensitiveContent", String(checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Show sensitive content in the table, Will use a spoiler to
|
||||
hide sensitive content if disabled.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<Label>Auto Download Settings</Label>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label htmlFor="limit">Limit Per Account</Label>
|
||||
<Input
|
||||
id="limit"
|
||||
className="w-24"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={settings?.autoDownloadLimit ?? 5}
|
||||
onChange={(e) => {
|
||||
void setSetting("autoDownloadLimit", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-1 justify-end">
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
<TabsContent value="general" className="h-full">
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<SettingsForm />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="statistics"
|
||||
className="flex flex-col overflow-y-scroll"
|
||||
>
|
||||
{accountId ? (
|
||||
<FileStatistics telegramId={accountId} />
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Please select an account to view statistics
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<TabsContent value="statistics" className="h-full">
|
||||
<div className="flex flex-col overflow-y-scroll">
|
||||
{accountId ? (
|
||||
<FileStatistics telegramId={accountId} />
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Please select an account to view statistics
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="proxys" className="h-full">
|
||||
<div className="flex h-full flex-col overflow-y-scroll">
|
||||
<Proxys
|
||||
telegramId={accountId}
|
||||
proxyName={account?.proxy}
|
||||
enableSelect={true}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
|
|
|
|||
142
web/src/components/settings-form.tsx
Normal file
142
web/src/components/settings-form.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { Bell } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React, { type FormEvent } from "react";
|
||||
import { useSettings } from "@/hooks/use-settings";
|
||||
|
||||
export default function SettingsForm() {
|
||||
const { settings, setSetting, updateSettings } = useSettings();
|
||||
|
||||
const imageLoadSizeOptions = [
|
||||
{ value: "s", label: "box 100x100" },
|
||||
{ value: "m", label: "box 320x320" },
|
||||
{ value: "x", label: "box 800x800" },
|
||||
{ value: "y", label: "box 1280x1280" },
|
||||
{ value: "w", label: "box 2560x2560" },
|
||||
{ value: "a", label: "crop 160x160" },
|
||||
{ value: "b", label: "crop 320x320" },
|
||||
{ value: "c", label: "crop 640x640" },
|
||||
{ value: "d", label: "crop 1280x1280" },
|
||||
];
|
||||
|
||||
const handleSave = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
await updateSettings();
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSave}
|
||||
className="flex h-full flex-col"
|
||||
>
|
||||
<div className="flex flex-1 flex-col space-y-4 overflow-y-scroll">
|
||||
<p className="rounded-md bg-gray-50 p-2 text-sm text-muted-foreground shadow">
|
||||
<Bell className="mr-2 inline-block h-4 w-4" />
|
||||
These settings will be applied to all accounts.
|
||||
</p>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="unique-only">Unique Only</Label>
|
||||
<Checkbox
|
||||
id="unique-only"
|
||||
checked={settings?.uniqueOnly === "true"}
|
||||
onCheckedChange={(checked) =>
|
||||
void setSetting("uniqueOnly", String(checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Show only unique file in the table. If disabled, will show all.{" "}
|
||||
<br />
|
||||
<strong>Warning:</strong> If enabled, the number of documents on the
|
||||
form will be inaccurate.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="need-load-preview-images">
|
||||
Need Load Preview Images
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="need-load-preview-images"
|
||||
checked={settings?.needToLoadImages === "true"}
|
||||
onCheckedChange={(checked) => {
|
||||
void setSetting("needToLoadImages", String(checked));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{settings?.needToLoadImages === "true" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="image-load-size">Load Size</Label>
|
||||
<Select
|
||||
value={settings.imageLoadSize}
|
||||
onValueChange={(v) => void setSetting("imageLoadSize", v)}
|
||||
>
|
||||
<SelectTrigger id="image-load-size">
|
||||
<SelectValue placeholder="Select Image Load Size" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{imageLoadSizeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The size of the image to load in the browser.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="show-sensitive-content">
|
||||
Show Sensitive Content
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="show-sensitive-content"
|
||||
checked={settings?.showSensitiveContent === "true"}
|
||||
onCheckedChange={(checked) =>
|
||||
void setSetting("showSensitiveContent", String(checked))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Show sensitive content in the table, Will use a spoiler to hide
|
||||
sensitive content if disabled.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||
<Label>Auto Download Settings</Label>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label htmlFor="limit">Limit Per Account</Label>
|
||||
<Input
|
||||
id="limit"
|
||||
className="w-24"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={settings?.autoDownloadLimit ?? 5}
|
||||
onChange={(e) => {
|
||||
void setSetting("autoDownloadLimit", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ interface SettingsContextType {
|
|||
isLoading: boolean;
|
||||
settings?: Settings;
|
||||
setSetting: (key: SettingKey, value: string) => Promise<void>;
|
||||
updateSettings: (updates?: Settings) => Promise<void>;
|
||||
updateSettings: (updates?: Partial<Settings>) => Promise<void>;
|
||||
}
|
||||
|
||||
const SettingsContext = createContext<SettingsContextType | undefined>(
|
||||
|
|
@ -41,13 +41,15 @@ export const SettingsProvider: React.FC<SettingsProviderProps> = ({
|
|||
);
|
||||
|
||||
// 更新配置项方法
|
||||
const updateSettings = async (updates?: Settings) => {
|
||||
const updateSettings = async (updates?: Partial<Settings>) => {
|
||||
await request("/settings/create", {
|
||||
body: updates ? JSON.stringify(updates) : JSON.stringify(settings),
|
||||
method: "POST",
|
||||
});
|
||||
// 更新缓存
|
||||
if (updates) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
await mutate((current) => ({ ...current, ...updates }), false);
|
||||
}
|
||||
toast({ title: "Success", description: "Settings updated" });
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export type TelegramAccount = {
|
|||
avatar?: string;
|
||||
status: "active" | "inactive";
|
||||
lastAuthorizationState?: TelegramObject;
|
||||
proxy?: string;
|
||||
};
|
||||
|
||||
export type TelegramChat = {
|
||||
|
|
@ -85,8 +86,20 @@ export const SettingKeys = [
|
|||
"imageLoadSize",
|
||||
"showSensitiveContent",
|
||||
"autoDownloadLimit",
|
||||
"proxys",
|
||||
] as const;
|
||||
|
||||
export type SettingKey = (typeof SettingKeys)[number];
|
||||
|
||||
export type Settings = Record<SettingKey, string>;
|
||||
|
||||
export type Proxy = {
|
||||
id?: string;
|
||||
name: string;
|
||||
server: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
type: "http" | "socks5";
|
||||
isEnabled?: boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue