pc 프로그램 다운로드 기능 적용
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
package com.goldenchart.controller;
|
||||||
|
|
||||||
|
import com.goldenchart.dto.DesktopAppReleaseInfoDto;
|
||||||
|
import com.goldenchart.service.DesktopAppReleaseService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/desktop-app")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DesktopAppReleaseController {
|
||||||
|
|
||||||
|
private final DesktopAppReleaseService releaseService;
|
||||||
|
|
||||||
|
@GetMapping("/info")
|
||||||
|
public DesktopAppReleaseInfoDto info(HttpServletRequest request) {
|
||||||
|
return releaseService.getInfo(resolvePublicBaseUrl(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolvePublicBaseUrl(HttpServletRequest request) {
|
||||||
|
String forwardedProto = request.getHeader("X-Forwarded-Proto");
|
||||||
|
String forwardedHost = request.getHeader("X-Forwarded-Host");
|
||||||
|
if (forwardedHost != null && !forwardedHost.isBlank()) {
|
||||||
|
String scheme = forwardedProto != null && !forwardedProto.isBlank() ? forwardedProto : "https";
|
||||||
|
return scheme + "://" + forwardedHost.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
String scheme = request.getScheme();
|
||||||
|
int port = request.getServerPort();
|
||||||
|
String host = request.getServerName();
|
||||||
|
boolean defaultPort = ("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443);
|
||||||
|
return defaultPort ? scheme + "://" + host : scheme + "://" + host + ":" + port;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.goldenchart.dto;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class DesktopAppPlatformReleaseDto {
|
||||||
|
private boolean available;
|
||||||
|
private String fileName;
|
||||||
|
private String version;
|
||||||
|
private Long sizeBytes;
|
||||||
|
private String updatedAt;
|
||||||
|
/** 브라우저 다운로드용 절대 URL */
|
||||||
|
private String downloadUrl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.goldenchart.dto;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class DesktopAppReleaseInfoDto {
|
||||||
|
private String version;
|
||||||
|
private String updatedAt;
|
||||||
|
private DesktopAppPlatformReleaseDto mac;
|
||||||
|
private DesktopAppPlatformReleaseDto windows;
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package com.goldenchart.service;
|
||||||
|
|
||||||
|
import com.goldenchart.dto.DesktopAppPlatformReleaseDto;
|
||||||
|
import com.goldenchart.dto.DesktopAppReleaseInfoDto;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class DesktopAppReleaseService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DT_FMT =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.of("Asia/Seoul"));
|
||||||
|
|
||||||
|
@Value("${goldenchart.desktop-app.release-dir:data/desktop-releases}")
|
||||||
|
private String releaseDir;
|
||||||
|
|
||||||
|
@Value("${goldenchart.desktop-app.version:}")
|
||||||
|
private String configuredVersion;
|
||||||
|
|
||||||
|
@Value("${goldenchart.desktop-app.public-base-url:}")
|
||||||
|
private String configuredPublicBaseUrl;
|
||||||
|
|
||||||
|
public DesktopAppReleaseInfoDto getInfo(String requestBaseUrl) {
|
||||||
|
String publicBaseUrl = resolvePublicBaseUrl(requestBaseUrl);
|
||||||
|
String base = normalizeBase(publicBaseUrl);
|
||||||
|
String staticBase = base + "/desktop/updates";
|
||||||
|
|
||||||
|
Optional<Path> macFile = resolveMacInstaller();
|
||||||
|
Optional<Path> winFile = resolveWindowsInstaller();
|
||||||
|
|
||||||
|
String version = configuredVersion != null && !configuredVersion.isBlank()
|
||||||
|
? configuredVersion.trim()
|
||||||
|
: resolveVersion(macFile, winFile);
|
||||||
|
|
||||||
|
Instant latestUpdated = Stream.of(macFile, winFile)
|
||||||
|
.flatMap(Optional::stream)
|
||||||
|
.map(this::lastModified)
|
||||||
|
.max(Comparator.naturalOrder())
|
||||||
|
.orElse(Instant.EPOCH);
|
||||||
|
|
||||||
|
return DesktopAppReleaseInfoDto.builder()
|
||||||
|
.version(version)
|
||||||
|
.updatedAt(latestUpdated.equals(Instant.EPOCH) ? null : DT_FMT.format(latestUpdated))
|
||||||
|
.mac(buildPlatform(macFile, staticBase, version))
|
||||||
|
.windows(buildPlatform(winFile, staticBase, version))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DesktopAppPlatformReleaseDto buildPlatform(
|
||||||
|
Optional<Path> file,
|
||||||
|
String staticBase,
|
||||||
|
String version
|
||||||
|
) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return DesktopAppPlatformReleaseDto.builder().available(false).build();
|
||||||
|
}
|
||||||
|
Path path = file.get();
|
||||||
|
try {
|
||||||
|
long size = Files.size(path);
|
||||||
|
Instant updated = Files.getLastModifiedTime(path).toInstant();
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
return DesktopAppPlatformReleaseDto.builder()
|
||||||
|
.available(true)
|
||||||
|
.fileName(fileName)
|
||||||
|
.version(version)
|
||||||
|
.sizeBytes(size)
|
||||||
|
.updatedAt(DT_FMT.format(updated))
|
||||||
|
.downloadUrl(staticBase + "/" + encodeFileName(fileName))
|
||||||
|
.build();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[desktop-app] stat failed: {}", e.getMessage());
|
||||||
|
return DesktopAppPlatformReleaseDto.builder().available(false).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant lastModified(Path path) {
|
||||||
|
try {
|
||||||
|
return Files.getLastModifiedTime(path).toInstant();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return Instant.EPOCH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveVersion(Optional<Path> macFile, Optional<Path> winFile) {
|
||||||
|
return Stream.of(macFile, winFile)
|
||||||
|
.flatMap(Optional::stream)
|
||||||
|
.map(p -> p.getFileName().toString())
|
||||||
|
.map(this::extractVersionFromFileName)
|
||||||
|
.filter(v -> v != null && !v.isBlank())
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractVersionFromFileName(String fileName) {
|
||||||
|
// GoldenChart_0.1.0_aarch64.dmg, GoldenChart_0.1.0_x64-setup.exe
|
||||||
|
var m = java.util.regex.Pattern
|
||||||
|
.compile("GoldenChart[_-](\\d+\\.\\d+\\.\\d+)", java.util.regex.Pattern.CASE_INSENSITIVE)
|
||||||
|
.matcher(fileName);
|
||||||
|
return m.find() ? m.group(1) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Path> resolveMacInstaller() {
|
||||||
|
Path dir = releaseDirectory();
|
||||||
|
if (dir == null) return Optional.empty();
|
||||||
|
try (Stream<Path> stream = Files.list(dir)) {
|
||||||
|
return stream
|
||||||
|
.filter(p -> Files.isRegularFile(p) && p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".dmg"))
|
||||||
|
.max(macComparator());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[desktop-app] list dir failed: {}", e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Path> resolveWindowsInstaller() {
|
||||||
|
Path dir = releaseDirectory();
|
||||||
|
if (dir == null) return Optional.empty();
|
||||||
|
try (Stream<Path> stream = Files.list(dir)) {
|
||||||
|
return stream
|
||||||
|
.filter(p -> {
|
||||||
|
String n = p.getFileName().toString().toLowerCase(Locale.ROOT);
|
||||||
|
return Files.isRegularFile(p) && n.endsWith(".exe")
|
||||||
|
&& (n.contains("setup") || n.contains("nsis") || n.startsWith("goldenchart"));
|
||||||
|
})
|
||||||
|
.max(windowsComparator());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[desktop-app] list dir failed: {}", e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<Path> macComparator() {
|
||||||
|
return Comparator
|
||||||
|
.comparingInt((Path p) -> scoreMac(p.getFileName().toString()))
|
||||||
|
.thenComparing(p -> lastModified(p).toEpochMilli());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<Path> windowsComparator() {
|
||||||
|
return Comparator
|
||||||
|
.comparingInt((Path p) -> scoreWindows(p.getFileName().toString()))
|
||||||
|
.thenComparing(p -> lastModified(p).toEpochMilli());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scoreMac(String name) {
|
||||||
|
String lower = name.toLowerCase(Locale.ROOT);
|
||||||
|
if (lower.contains("aarch64") || lower.contains("arm64")) return 3;
|
||||||
|
if (lower.contains("universal")) return 2;
|
||||||
|
if (lower.contains("x64") || lower.contains("x86_64")) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scoreWindows(String name) {
|
||||||
|
String lower = name.toLowerCase(Locale.ROOT);
|
||||||
|
if (lower.contains("setup")) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path releaseDirectory() {
|
||||||
|
Path dir = Paths.get(releaseDir).toAbsolutePath().normalize();
|
||||||
|
if (!Files.isDirectory(dir)) {
|
||||||
|
log.debug("[desktop-app] release dir missing: {}", dir);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String encodeFileName(String fileName) {
|
||||||
|
return fileName.replace(" ", "%20");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePublicBaseUrl(String requestBaseUrl) {
|
||||||
|
if (configuredPublicBaseUrl != null && !configuredPublicBaseUrl.isBlank()) {
|
||||||
|
return configuredPublicBaseUrl.trim();
|
||||||
|
}
|
||||||
|
if (requestBaseUrl != null && !requestBaseUrl.isBlank()) {
|
||||||
|
return requestBaseUrl.trim();
|
||||||
|
}
|
||||||
|
return "http://exdev.co.kr";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeBase(String publicBaseUrl) {
|
||||||
|
if (publicBaseUrl == null || publicBaseUrl.isBlank()) {
|
||||||
|
return "http://exdev.co.kr";
|
||||||
|
}
|
||||||
|
return publicBaseUrl.replaceAll("/+$", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,6 +112,10 @@ goldenchart:
|
|||||||
version: ${GC_MOBILE_APP_VERSION:}
|
version: ${GC_MOBILE_APP_VERSION:}
|
||||||
# QR·APK 다운로드 절대 URL (로컬 개발 시에도 exdev 서버 APK 사용)
|
# QR·APK 다운로드 절대 URL (로컬 개발 시에도 exdev 서버 APK 사용)
|
||||||
public-base-url: ${GC_MOBILE_APP_PUBLIC_BASE_URL:http://exdev.co.kr}
|
public-base-url: ${GC_MOBILE_APP_PUBLIC_BASE_URL:http://exdev.co.kr}
|
||||||
|
desktop-app:
|
||||||
|
release-dir: ${GC_DESKTOP_APP_RELEASE_DIR:data/desktop-releases}
|
||||||
|
version: ${GC_DESKTOP_APP_VERSION:}
|
||||||
|
public-base-url: ${GC_DESKTOP_APP_PUBLIC_BASE_URL:http://exdev.co.kr}
|
||||||
cors:
|
cors:
|
||||||
allowed-origins:
|
allowed-origins:
|
||||||
- http://localhost:5173
|
- http://localhost:5173
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
# GoldenChart Desktop 설치 파일
|
||||||
|
|
||||||
|
Jenkins 빌드 후 `scripts/publish-desktop-update.sh`가 이 디렉터리와
|
||||||
|
`frontend/public/desktop/updates/`에 설치 파일을 복사합니다.
|
||||||
|
|
||||||
|
| 파일 | 플랫폼 |
|
||||||
|
|------|--------|
|
||||||
|
| `GoldenChart_*_aarch64.dmg` | macOS |
|
||||||
|
| `GoldenChart_*-setup.exe` | Windows |
|
||||||
|
|
||||||
|
- API: `GET /api/desktop-app/info`
|
||||||
|
- 다운로드 URL: `http://exdev.co.kr/desktop/updates/<파일명>`
|
||||||
@@ -1 +1 @@
|
|||||||
VITE_API_BASE_URL=https://exdev.co.kr/api
|
VITE_API_BASE_URL=http://exdev.co.kr/api
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@stomp/stompjs": "^7.3.0",
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"@tanstack/react-virtual": "^3.14.2",
|
"@tanstack/react-virtual": "^3.14.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.9",
|
||||||
"@tauri-apps/plugin-notification": "^2",
|
"@tauri-apps/plugin-notification": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-process": "^2",
|
"@tauri-apps/plugin-process": "^2",
|
||||||
|
|||||||
Generated
+350
-7
@@ -462,6 +462,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -499,10 +505,39 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
"time",
|
"time",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"document-features",
|
||||||
|
"idna",
|
||||||
|
"log",
|
||||||
|
"publicsuffix",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -526,7 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -539,7 +574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -659,6 +694,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-url"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.9.11"
|
version = "0.9.11"
|
||||||
@@ -788,6 +829,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dom_query"
|
name = "dom_query"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
@@ -874,6 +924,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1254,8 +1313,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1265,9 +1326,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1387,6 +1450,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
@@ -1445,6 +1509,25 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1549,6 +1632,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1572,6 +1656,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1592,9 +1677,11 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2026,6 +2113,12 @@ version = "0.8.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2041,6 +2134,12 @@ version = "0.4.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac-notification-sys"
|
name = "mac-notification-sys"
|
||||||
version = "0.6.14"
|
version = "0.6.14"
|
||||||
@@ -2775,6 +2874,22 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psl-types"
|
||||||
|
version = "2.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "publicsuffix"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
|
||||||
|
dependencies = [
|
||||||
|
"idna",
|
||||||
|
"psl-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.37.5"
|
version = "0.37.5"
|
||||||
@@ -2793,6 +2908,61 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"lru-slab",
|
||||||
|
"rand",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -2918,6 +3088,49 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"cookie_store",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@@ -3031,6 +3244,7 @@ version = "1.14.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3040,7 +3254,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"jni 0.22.4",
|
"jni 0.22.4",
|
||||||
"log",
|
"log",
|
||||||
@@ -3078,6 +3292,12 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -3160,7 +3380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
@@ -3300,6 +3520,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.21.0"
|
||||||
@@ -3582,6 +3814,27 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.2.2"
|
version = "6.2.2"
|
||||||
@@ -3603,7 +3856,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dbus",
|
"dbus",
|
||||||
@@ -3693,7 +3946,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -3792,6 +4045,54 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs"
|
||||||
|
version = "2.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"log",
|
||||||
|
"objc2-foundation",
|
||||||
|
"percent-encoding",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-http"
|
||||||
|
version = "2.5.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5bd512048e1985b7ec78f96d99083e2ddaf7e0d906b2b63c44ce5bb8b894067"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cookie_store",
|
||||||
|
"data-url",
|
||||||
|
"http",
|
||||||
|
"regex",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"urlpattern",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-notification"
|
name = "tauri-plugin-notification"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
@@ -3859,7 +4160,7 @@ dependencies = [
|
|||||||
"minisign-verify",
|
"minisign-verify",
|
||||||
"osakit",
|
"osakit",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"rustls",
|
"rustls",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4117,9 +4418,21 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -4691,6 +5004,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web_atoms"
|
name = "web_atoms"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -4756,6 +5079,15 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -4941,6 +5273,17 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
"windows-result 0.4.1",
|
||||||
|
"windows-strings 0.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ tauri-plugin-notification = "2"
|
|||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-http = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -22,6 +22,16 @@
|
|||||||
"updater:allow-download-and-install",
|
"updater:allow-download-and-install",
|
||||||
"process:allow-restart",
|
"process:allow-restart",
|
||||||
"process:allow-exit",
|
"process:allow-exit",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{ "url": "http://exdev.co.kr/**" },
|
||||||
|
{ "url": "http://localhost:8080/**" },
|
||||||
|
{ "url": "http://127.0.0.1:8080/**" },
|
||||||
|
{ "url": "https://exdev.co.kr/**" },
|
||||||
|
{ "url": "https://api.upbit.com/**" }
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ fn show_main_window(app: &AppHandle) {
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DESKTOP_API_BASE, setApiBase } from '@goldenchart/shared';
|
import { DESKTOP_API_BASE, setApiBase } from '@goldenchart/shared';
|
||||||
|
|
||||||
/** Tauri desktop — 항상 https exdev (localhost·http 금지) */
|
/** Tauri desktop — 항상 exdev 서버 (localhost 금지) */
|
||||||
export function ensureDesktopApiBase(): void {
|
export function ensureDesktopApiBase(): void {
|
||||||
setApiBase(DESKTOP_API_BASE);
|
setApiBase(DESKTOP_API_BASE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
|
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
|
||||||
|
import { installTauriHttp } from './installTauriHttp';
|
||||||
|
|
||||||
/** App·backendApi import 전 API base 고정 (localhost 금지) */
|
/** App·backendApi import 전 API base 고정 + 네이티브 HTTP */
|
||||||
ensureDesktopApiBase();
|
ensureDesktopApiBase();
|
||||||
|
installTauriHttp();
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||||
|
import { setHttpFetch } from '@goldenchart/shared';
|
||||||
|
|
||||||
|
/** WebView fetch(ATS/CORS) 대신 Rust reqwest — exdev HTTP API */
|
||||||
|
export function installTauriHttp(): void {
|
||||||
|
setHttpFetch(tauriFetch);
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
global: 'globalThis',
|
global: 'globalThis',
|
||||||
/** desktop은 항상 exdev 서버 (localhost 금지) */
|
/** desktop은 항상 exdev 서버 (localhost 금지) */
|
||||||
'import.meta.env.VITE_API_BASE_URL': JSON.stringify('https://exdev.co.kr/api'),
|
'import.meta.env.VITE_API_BASE_URL': JSON.stringify('http://exdev.co.kr/api'),
|
||||||
__DESKTOP_CLIENT__: 'true',
|
__DESKTOP_CLIENT__: 'true',
|
||||||
},
|
},
|
||||||
envDir: __dirname,
|
envDir: __dirname,
|
||||||
|
|||||||
Binary file not shown.
@@ -13231,6 +13231,41 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
|
|||||||
margin-top: 12px; font-size: 11px; color: var(--text3); line-height: 1.45;
|
margin-top: 12px; font-size: 11px; color: var(--text3); line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-download-tabs {
|
||||||
|
display: flex; gap: 6px; margin-bottom: 14px;
|
||||||
|
padding: 4px; border-radius: 12px; background: var(--bg3); border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.app-download-tab {
|
||||||
|
flex: 1; padding: 10px 12px; border: none; border-radius: 9px;
|
||||||
|
background: transparent; color: var(--text2); font-size: 13px; font-weight: 600; cursor: pointer;
|
||||||
|
}
|
||||||
|
.app-download-tab.active {
|
||||||
|
background: var(--bg); color: var(--text);
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.app-download-app-icon--desktop {
|
||||||
|
background: linear-gradient(135deg, #2196f3, #0ea5e9);
|
||||||
|
}
|
||||||
|
.app-download-pc-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
|
||||||
|
.app-download-pc-card {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
padding: 12px 14px; border-radius: 12px;
|
||||||
|
background: var(--bg2); border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.app-download-pc-card--empty { opacity: 0.75; }
|
||||||
|
.app-download-pc-icon { font-size: 28px; flex-shrink: 0; width: 36px; text-align: center; }
|
||||||
|
.app-download-pc-body { flex: 1; min-width: 0; }
|
||||||
|
.app-download-pc-body strong { display: block; font-size: 14px; margin-bottom: 4px; }
|
||||||
|
.app-download-pc-btn {
|
||||||
|
flex-shrink: 0; padding: 8px 14px; border: none; border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #2196f3, #0284c7);
|
||||||
|
color: #fff; font-size: 13px; font-weight: 700; cursor: pointer;
|
||||||
|
}
|
||||||
|
.app-download-pc-btn:hover { filter: brightness(1.06); }
|
||||||
|
.tmb-desktop-update-btn.busy { opacity: 0.65; cursor: wait; }
|
||||||
|
.tmb-desktop-update-btn.busy svg { animation: tmb-update-spin 0.9s linear infinite; }
|
||||||
|
@keyframes tmb-update-spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
/* ── 정식 로그인 유도 (게스트·매매 기능) ─────────────────────────────────── */
|
/* ── 정식 로그인 유도 (게스트·매매 기능) ─────────────────────────────────── */
|
||||||
.formal-login-gate {
|
.formal-login-gate {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,28 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* 모바일 앱 설치 — APK 다운로드 URL·QR 코드
|
* 모바일·PC 프로그램 설치 — 탭 UI (모바일 QR / PC dmg·exe)
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import DraggableModalFrame from './DraggableModalFrame';
|
import DraggableModalFrame from './DraggableModalFrame';
|
||||||
import { loadMobileAppReleaseInfo, type MobileAppReleaseInfoDto } from '../utils/backendApi';
|
import {
|
||||||
|
loadDesktopAppReleaseInfo,
|
||||||
|
loadMobileAppReleaseInfo,
|
||||||
|
type DesktopAppPlatformReleaseDto,
|
||||||
|
type DesktopAppReleaseInfoDto,
|
||||||
|
type MobileAppReleaseInfoDto,
|
||||||
|
} from '../utils/backendApi';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TabId = 'mobile' | 'desktop';
|
||||||
|
|
||||||
function formatBytes(n?: number): string {
|
function formatBytes(n?: number): string {
|
||||||
if (n == null || !Number.isFinite(n)) return '—';
|
if (n == null || !Number.isFinite(n)) return '—';
|
||||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||||
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** APK·QR은 항상 서버가 내려준 절대 URL 사용 (exdev.co.kr, localhost 아님) */
|
|
||||||
function resolveDownloadUrl(info: MobileAppReleaseInfoDto | null): string {
|
function resolveDownloadUrl(info: MobileAppReleaseInfoDto | null): string {
|
||||||
if (!info?.available) return '';
|
if (!info?.available) return '';
|
||||||
return info.downloadUrl?.trim() ?? '';
|
return info.downloadUrl?.trim() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppDownloadModal: React.FC<Props> = ({ onClose }) => {
|
function triggerDownload(url: string, fileName: string) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
a.rel = 'noopener';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobileTab: React.FC = () => {
|
||||||
const [info, setInfo] = useState<MobileAppReleaseInfoDto | null>(null);
|
const [info, setInfo] = useState<MobileAppReleaseInfoDto | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||||
@@ -33,15 +50,9 @@ const AppDownloadModal: React.FC<Props> = ({ onClose }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void loadMobileAppReleaseInfo()
|
void loadMobileAppReleaseInfo()
|
||||||
.then(data => {
|
.then(data => { if (!cancelled) setInfo(data); })
|
||||||
if (!cancelled) setInfo(data);
|
.catch(() => { if (!cancelled) setError('앱 정보를 불러오지 못했습니다.'); })
|
||||||
})
|
.finally(() => { if (!cancelled) setLoading(false); });
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setError('앱 정보를 불러오지 못했습니다.');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setLoading(false);
|
|
||||||
});
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -55,94 +66,224 @@ const AppDownloadModal: React.FC<Props> = ({ onClose }) => {
|
|||||||
width: 220,
|
width: 220,
|
||||||
margin: 2,
|
margin: 2,
|
||||||
color: { dark: '#111827', light: '#ffffff' },
|
color: { dark: '#111827', light: '#ffffff' },
|
||||||
}).then(url => {
|
}).then(url => { if (!cancelled) setQrDataUrl(url); })
|
||||||
if (!cancelled) setQrDataUrl(url);
|
.catch(() => { if (!cancelled) setQrDataUrl(null); });
|
||||||
}).catch(() => {
|
|
||||||
if (!cancelled) setQrDataUrl(null);
|
|
||||||
});
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [downloadUrl]);
|
}, [downloadUrl]);
|
||||||
|
|
||||||
const handleDownload = () => {
|
if (loading) return <p className="app-download-muted">앱 정보 불러오는 중…</p>;
|
||||||
if (!downloadUrl) return;
|
if (error) return <p className="app-download-error">{error}</p>;
|
||||||
const a = document.createElement('a');
|
if (!info?.available) {
|
||||||
a.href = downloadUrl;
|
return (
|
||||||
a.download = info?.fileName ?? 'goldenchart-android.apk';
|
<div className="app-download-empty">
|
||||||
a.rel = 'noopener';
|
<div className="app-download-empty-icon" aria-hidden>📱</div>
|
||||||
document.body.appendChild(a);
|
<h3>설치 파일 준비 중</h3>
|
||||||
a.click();
|
<p>
|
||||||
a.remove();
|
Android APK가 아직 서버에 업로드되지 않았습니다.
|
||||||
};
|
<br />
|
||||||
|
<code>data/mobile-releases/goldenchart-android.apk</code> 경로에 빌드 파일을 배치하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="app-download-hero">
|
||||||
|
<div className="app-download-app-icon" aria-hidden>GC</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="app-download-app-name">GoldenChart Mobile</h3>
|
||||||
|
<p className="app-download-meta">
|
||||||
|
{info.version && <span>v{info.version}</span>}
|
||||||
|
{info.sizeBytes != null && <span>{formatBytes(info.sizeBytes)}</span>}
|
||||||
|
{info.updatedAt && <span>{info.updatedAt}</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="app-download-qr-block">
|
||||||
|
{qrDataUrl ? (
|
||||||
|
<img src={qrDataUrl} alt="앱 설치 QR 코드" className="app-download-qr" width={220} height={220} />
|
||||||
|
) : (
|
||||||
|
<div className="app-download-qr app-download-qr--placeholder">QR 생성 중…</div>
|
||||||
|
)}
|
||||||
|
<div className="app-download-qr-help">
|
||||||
|
<strong>휴대폰 카메라로 QR 스캔</strong>
|
||||||
|
<p>Android에서 APK 다운로드 페이지가 열립니다. 다운로드 후 설치하세요.</p>
|
||||||
|
<p className="app-download-muted app-download-url" title={downloadUrl}>{downloadUrl}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="app-download-btn-primary"
|
||||||
|
onClick={() => triggerDownload(downloadUrl, info.fileName ?? 'goldenchart-android.apk')}
|
||||||
|
>
|
||||||
|
Android APK 다운로드
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<details className="app-download-steps">
|
||||||
|
<summary>설치 방법 (Android)</summary>
|
||||||
|
<ol>
|
||||||
|
<li>QR 코드를 스캔하거나 위 버튼으로 APK를 다운로드합니다.</li>
|
||||||
|
<li>「출처를 알 수 없는 앱」 설치 허용이 필요할 수 있습니다.</li>
|
||||||
|
<li>다운로드 완료 후 APK 파일을 탭하여 설치합니다.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<p className="app-download-ios-note">
|
||||||
|
iOS는 App Store / TestFlight 배포가 필요합니다. 현재 QR·APK 설치는 Android 전용입니다.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlatformCard: React.FC<{
|
||||||
|
platform: 'mac' | 'windows';
|
||||||
|
release?: DesktopAppPlatformReleaseDto;
|
||||||
|
}> = ({ platform, release }) => {
|
||||||
|
const isMac = platform === 'mac';
|
||||||
|
const label = isMac ? 'macOS (Apple Silicon · Intel)' : 'Windows (64-bit)';
|
||||||
|
const icon = isMac ? '🍎' : '🪟';
|
||||||
|
const ext = isMac ? '.dmg' : '.exe';
|
||||||
|
|
||||||
|
if (!release?.available || !release.downloadUrl) {
|
||||||
|
return (
|
||||||
|
<div className="app-download-pc-card app-download-pc-card--empty">
|
||||||
|
<div className="app-download-pc-icon" aria-hidden>{icon}</div>
|
||||||
|
<div>
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<p className="app-download-muted">설치 파일 준비 중 ({ext})</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-download-pc-card">
|
||||||
|
<div className="app-download-pc-icon" aria-hidden>{icon}</div>
|
||||||
|
<div className="app-download-pc-body">
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<p className="app-download-meta">
|
||||||
|
{release.version && <span>v{release.version}</span>}
|
||||||
|
{release.sizeBytes != null && <span>{formatBytes(release.sizeBytes)}</span>}
|
||||||
|
{release.updatedAt && <span>{release.updatedAt}</span>}
|
||||||
|
</p>
|
||||||
|
<p className="app-download-muted app-download-url" title={release.downloadUrl}>
|
||||||
|
{release.fileName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="app-download-pc-btn"
|
||||||
|
onClick={() => triggerDownload(release.downloadUrl!, release.fileName ?? `GoldenChart${ext}`)}
|
||||||
|
>
|
||||||
|
다운로드
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DesktopTab: React.FC = () => {
|
||||||
|
const [info, setInfo] = useState<DesktopAppReleaseInfoDto | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
void loadDesktopAppReleaseInfo()
|
||||||
|
.then(data => { if (!cancelled) setInfo(data); })
|
||||||
|
.catch(() => { if (!cancelled) setError('PC 프로그램 정보를 불러오지 못했습니다.'); })
|
||||||
|
.finally(() => { if (!cancelled) setLoading(false); });
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasAny = info?.mac?.available || info?.windows?.available;
|
||||||
|
|
||||||
|
if (loading) return <p className="app-download-muted">PC 프로그램 정보 불러오는 중…</p>;
|
||||||
|
if (error) return <p className="app-download-error">{error}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="app-download-hero">
|
||||||
|
<div className="app-download-app-icon app-download-app-icon--desktop" aria-hidden>GC</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="app-download-app-name">GoldenChart Desktop</h3>
|
||||||
|
<p className="app-download-meta">
|
||||||
|
{info?.version && <span>v{info.version}</span>}
|
||||||
|
{info?.updatedAt && <span>{info.updatedAt}</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!hasAny && (
|
||||||
|
<div className="app-download-empty">
|
||||||
|
<div className="app-download-empty-icon" aria-hidden>💻</div>
|
||||||
|
<h3>설치 파일 준비 중</h3>
|
||||||
|
<p>
|
||||||
|
macOS(.dmg) 또는 Windows(.exe) 설치 파일이 아직 서버에 없습니다.
|
||||||
|
<br />
|
||||||
|
Jenkins 빌드 후 <code>scripts/publish-desktop-update.sh</code>를 실행하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasAny && (
|
||||||
|
<>
|
||||||
|
<div className="app-download-pc-list">
|
||||||
|
<PlatformCard platform="mac" release={info?.mac} />
|
||||||
|
<PlatformCard platform="windows" release={info?.windows} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className="app-download-steps">
|
||||||
|
<summary>설치 방법</summary>
|
||||||
|
<ol>
|
||||||
|
<li><strong>macOS</strong>: dmg를 열고 GoldenChart.app을 Applications 폴더로 드래그합니다.</li>
|
||||||
|
<li><strong>Windows</strong>: setup.exe를 실행하고 안내에 따라 설치합니다.</li>
|
||||||
|
<li>설치된 PC 앱은 메뉴 우측 상단 <strong>업데이트</strong> 버튼으로 자동 업데이트할 수 있습니다.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppDownloadModal: React.FC<Props> = ({ onClose }) => {
|
||||||
|
const [tab, setTab] = useState<TabId>('mobile');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DraggableModalFrame
|
<DraggableModalFrame
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Mobile App"
|
title="Apps"
|
||||||
titleKo="GoldenChart 앱 설치"
|
titleKo="GoldenChart 앱·PC 프로그램"
|
||||||
badge="Android"
|
badge={tab === 'mobile' ? 'Mobile' : 'Desktop'}
|
||||||
width={440}
|
width={480}
|
||||||
dialogClassName="sp-modal app-download-modal"
|
dialogClassName="sp-modal app-download-modal"
|
||||||
>
|
>
|
||||||
<div className="app-download-body">
|
<div className="app-download-tabs" role="tablist" aria-label="다운로드 종류">
|
||||||
{loading && <p className="app-download-muted">앱 정보 불러오는 중…</p>}
|
<button
|
||||||
{!loading && error && <p className="app-download-error">{error}</p>}
|
type="button"
|
||||||
{!loading && !error && !info?.available && (
|
role="tab"
|
||||||
<div className="app-download-empty">
|
aria-selected={tab === 'mobile'}
|
||||||
<div className="app-download-empty-icon" aria-hidden>📱</div>
|
className={`app-download-tab${tab === 'mobile' ? ' active' : ''}`}
|
||||||
<h3>설치 파일 준비 중</h3>
|
onClick={() => setTab('mobile')}
|
||||||
<p>
|
>
|
||||||
Android APK가 아직 서버에 업로드되지 않았습니다.
|
모바일
|
||||||
<br />
|
</button>
|
||||||
<code>data/mobile-releases/goldenchart-android.apk</code> 경로에 빌드 파일을 배치하세요.
|
<button
|
||||||
</p>
|
type="button"
|
||||||
</div>
|
role="tab"
|
||||||
)}
|
aria-selected={tab === 'desktop'}
|
||||||
{!loading && info?.available && (
|
className={`app-download-tab${tab === 'desktop' ? ' active' : ''}`}
|
||||||
<>
|
onClick={() => setTab('desktop')}
|
||||||
<div className="app-download-hero">
|
>
|
||||||
<div className="app-download-app-icon" aria-hidden>GC</div>
|
PC 프로그램
|
||||||
<div>
|
</button>
|
||||||
<h3 className="app-download-app-name">GoldenChart</h3>
|
</div>
|
||||||
<p className="app-download-meta">
|
|
||||||
{info.version && <span>v{info.version}</span>}
|
|
||||||
{info.sizeBytes != null && <span>{formatBytes(info.sizeBytes)}</span>}
|
|
||||||
{info.updatedAt && <span>{info.updatedAt}</span>}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="app-download-qr-block">
|
<div className="app-download-body" role="tabpanel">
|
||||||
{qrDataUrl ? (
|
{tab === 'mobile' ? <MobileTab /> : <DesktopTab />}
|
||||||
<img src={qrDataUrl} alt="앱 설치 QR 코드" className="app-download-qr" width={220} height={220} />
|
|
||||||
) : (
|
|
||||||
<div className="app-download-qr app-download-qr--placeholder">QR 생성 중…</div>
|
|
||||||
)}
|
|
||||||
<div className="app-download-qr-help">
|
|
||||||
<strong>휴대폰 카메라로 QR 스캔</strong>
|
|
||||||
<p>Android에서 APK 다운로드 페이지가 열립니다. 다운로드 후 설치하세요.</p>
|
|
||||||
<p className="app-download-muted app-download-url" title={downloadUrl}>{downloadUrl}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" className="app-download-btn-primary" onClick={handleDownload}>
|
|
||||||
Android APK 다운로드
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<details className="app-download-steps">
|
|
||||||
<summary>설치 방법 (Android)</summary>
|
|
||||||
<ol>
|
|
||||||
<li>QR 코드를 스캔하거나 위 버튼으로 APK를 다운로드합니다.</li>
|
|
||||||
<li>「출처를 알 수 없는 앱」 설치 허용이 필요할 수 있습니다.</li>
|
|
||||||
<li>다운로드 완료 후 APK 파일을 탭하여 설치합니다.</li>
|
|
||||||
<li>설치 후 앱에서 API 서버 URL을 설정하세요 (설정 › 네트워크).</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<p className="app-download-ios-note">
|
|
||||||
iOS는 App Store / TestFlight 배포가 필요합니다. 현재 QR·APK 설치는 Android 전용입니다.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</DraggableModalFrame>
|
</DraggableModalFrame>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
* 레이아웃:
|
* 레이아웃:
|
||||||
* [로고] | [대시보드] [실시간차트] [전략편집기] … | [테마토글] [로그인]
|
* [로고] | [대시보드] [실시간차트] [전략편집기] … | [테마토글] [로그인]
|
||||||
*/
|
*/
|
||||||
import React, { memo, useEffect, useState } from 'react';
|
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import type { Theme } from '../types';
|
import type { Theme } from '../types';
|
||||||
import type { AuthSession } from '../utils/auth';
|
import type { AuthSession } from '../utils/auth';
|
||||||
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
||||||
import { canAccessMenu } from '../utils/permissions';
|
import { canAccessMenu } from '../utils/permissions';
|
||||||
|
import { isDesktop } from '../utils/platform';
|
||||||
|
import { checkDesktopUpdates } from '../utils/desktopBridge';
|
||||||
|
|
||||||
export type MenuPage = 'dashboard' | 'chart' | 'paper' | 'virtual' | 'trend-search' | 'verification-board' | 'strategy' | 'strategy-editor' | 'strategy-evaluation' | 'backtest' | 'analysis-report' | 'notifications' | 'settings' | 'widget-dashboard';
|
export type MenuPage = 'dashboard' | 'chart' | 'paper' | 'virtual' | 'trend-search' | 'verification-board' | 'strategy' | 'strategy-editor' | 'strategy-evaluation' | 'backtest' | 'analysis-report' | 'notifications' | 'settings' | 'widget-dashboard';
|
||||||
|
|
||||||
@@ -105,6 +107,13 @@ const IcAppDownload = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const IcDesktopUpdate = () => (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9"/>
|
||||||
|
<polyline points="13.5,2.5 13.5,5.5 10.5,5.5"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const IcNotify = () => (
|
const IcNotify = () => (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M8 1.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1 4.5-1.5 4.5h12c-.5 0-1.5-1-1.5-4.5A4.5 4.5 0 0 0 8 1.5z"/>
|
<path d="M8 1.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1 4.5-1.5 4.5h12c-.5 0-1.5-1-1.5-4.5A4.5 4.5 0 0 0 8 1.5z"/>
|
||||||
@@ -257,6 +266,24 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
menuPermissions,
|
menuPermissions,
|
||||||
}: TopMenuBarProps) {
|
}: TopMenuBarProps) {
|
||||||
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
||||||
|
const [desktopUpdateBusy, setDesktopUpdateBusy] = useState(false);
|
||||||
|
const [desktopUpdateHint, setDesktopUpdateHint] = useState('');
|
||||||
|
const desktopClient = isDesktop();
|
||||||
|
|
||||||
|
const handleDesktopUpdate = useCallback(async () => {
|
||||||
|
if (desktopUpdateBusy) return;
|
||||||
|
setDesktopUpdateBusy(true);
|
||||||
|
setDesktopUpdateHint('업데이트 확인 중…');
|
||||||
|
try {
|
||||||
|
const res = await checkDesktopUpdates();
|
||||||
|
setDesktopUpdateHint(res.message ?? (res.available ? `v${res.version ?? ''} 설치` : '최신 버전'));
|
||||||
|
} catch {
|
||||||
|
setDesktopUpdateHint('업데이트 확인 실패');
|
||||||
|
} finally {
|
||||||
|
setDesktopUpdateBusy(false);
|
||||||
|
}
|
||||||
|
}, [desktopUpdateBusy]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sync = () => setIsFullscreen(!!document.fullscreenElement);
|
const sync = () => setIsFullscreen(!!document.fullscreenElement);
|
||||||
document.addEventListener('fullscreenchange', sync);
|
document.addEventListener('fullscreenchange', sync);
|
||||||
@@ -326,19 +353,28 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{onOpenAppDownload && (
|
{desktopClient ? (
|
||||||
<>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={`tmb-app-download-btn tmb-desktop-update-btn${desktopUpdateBusy ? ' busy' : ''}`}
|
||||||
className="tmb-app-download-btn"
|
onClick={() => { void handleDesktopUpdate(); }}
|
||||||
onClick={onOpenAppDownload}
|
disabled={desktopUpdateBusy}
|
||||||
title="GoldenChart 모바일 앱 다운로드"
|
title={desktopUpdateHint || 'PC 프로그램 업데이트'}
|
||||||
aria-label="앱 다운로드"
|
aria-label="PC 프로그램 업데이트"
|
||||||
>
|
>
|
||||||
<IcAppDownload />
|
<IcDesktopUpdate />
|
||||||
</button>
|
</button>
|
||||||
</>
|
) : onOpenAppDownload ? (
|
||||||
)}
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tmb-app-download-btn"
|
||||||
|
onClick={onOpenAppDownload}
|
||||||
|
title="GoldenChart 앱·PC 프로그램 다운로드"
|
||||||
|
aria-label="앱·PC 프로그램 다운로드"
|
||||||
|
>
|
||||||
|
<IcAppDownload />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{onFullscreen && (
|
{onFullscreen && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -643,6 +643,28 @@ export async function loadMobileAppReleaseInfo(): Promise<MobileAppReleaseInfoDt
|
|||||||
return request<MobileAppReleaseInfoDto>('/mobile-app/info');
|
return request<MobileAppReleaseInfoDto>('/mobile-app/info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── PC(데스크톱) 앱 배포 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface DesktopAppPlatformReleaseDto {
|
||||||
|
available: boolean;
|
||||||
|
fileName?: string;
|
||||||
|
version?: string;
|
||||||
|
sizeBytes?: number;
|
||||||
|
updatedAt?: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesktopAppReleaseInfoDto {
|
||||||
|
version?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
mac?: DesktopAppPlatformReleaseDto;
|
||||||
|
windows?: DesktopAppPlatformReleaseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadDesktopAppReleaseInfo(): Promise<DesktopAppReleaseInfoDto | null> {
|
||||||
|
return request<DesktopAppReleaseInfoDto>('/desktop-app/info');
|
||||||
|
}
|
||||||
|
|
||||||
// ── 모의투자 ───────────────────────────────────────────────────────────────────
|
// ── 모의투자 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface PaperPositionDto {
|
export interface PaperPositionDto {
|
||||||
|
|||||||
Generated
+10
@@ -50,6 +50,7 @@
|
|||||||
"@stomp/stompjs": "^7.3.0",
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"@tanstack/react-virtual": "^3.14.2",
|
"@tanstack/react-virtual": "^3.14.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.9",
|
||||||
"@tauri-apps/plugin-notification": "^2",
|
"@tauri-apps/plugin-notification": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-process": "^2",
|
"@tauri-apps/plugin-process": "^2",
|
||||||
@@ -1792,6 +1793,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-http": {
|
||||||
|
"version": "2.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz",
|
||||||
|
"integrity": "sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-notification": {
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export type StrategyFlowLayoutStore = Record<string, unknown>;
|
|||||||
/** 배포 APK·exdev (모바일 APK — http) */
|
/** 배포 APK·exdev (모바일 APK — http) */
|
||||||
export const PRODUCTION_API_BASE = 'http://exdev.co.kr/api';
|
export const PRODUCTION_API_BASE = 'http://exdev.co.kr/api';
|
||||||
|
|
||||||
/** Tauri desktop — macOS ATS 호환 (https 필수) */
|
/** Tauri desktop — exdev HTTP (HTTPS /api는 Caddy에서 빈 응답) */
|
||||||
export const DESKTOP_API_BASE = 'https://exdev.co.kr/api';
|
export const DESKTOP_API_BASE = 'http://exdev.co.kr/api';
|
||||||
|
|
||||||
function viteEnv(): Record<string, string> | undefined {
|
function viteEnv(): Record<string, string> | undefined {
|
||||||
return typeof import.meta !== 'undefined'
|
return typeof import.meta !== 'undefined'
|
||||||
@@ -43,7 +43,6 @@ function isDesktopClient(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveApiBase(): string {
|
function resolveApiBase(): string {
|
||||||
// Desktop(Tauri): https exdev (macOS ATS — http 차단)
|
|
||||||
if (isDesktopClient()) {
|
if (isDesktopClient()) {
|
||||||
return DESKTOP_API_BASE;
|
return DESKTOP_API_BASE;
|
||||||
}
|
}
|
||||||
@@ -128,6 +127,18 @@ const DEFAULT_FETCH_TIMEOUT_MS = 45_000;
|
|||||||
/** exdev 첫 로그인 응답이 60초 이상 걸리는 경우 있음 */
|
/** exdev 첫 로그인 응답이 60초 이상 걸리는 경우 있음 */
|
||||||
const LOGIN_FETCH_TIMEOUT_MS = 180_000;
|
const LOGIN_FETCH_TIMEOUT_MS = 180_000;
|
||||||
|
|
||||||
|
type HttpFetchFn = typeof fetch;
|
||||||
|
let httpFetchImpl: HttpFetchFn | null = null;
|
||||||
|
|
||||||
|
/** Tauri 등 WebView fetch 대신 네이티브 HTTP 사용 (desktop bootstrap에서 호출) */
|
||||||
|
export function setHttpFetch(fn: HttpFetchFn): void {
|
||||||
|
httpFetchImpl = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function httpFetch(): HttpFetchFn {
|
||||||
|
return httpFetchImpl ?? fetch;
|
||||||
|
}
|
||||||
|
|
||||||
function wrapNetworkError(e: unknown): Error {
|
function wrapNetworkError(e: unknown): Error {
|
||||||
if (e instanceof TypeError || (e instanceof Error && /failed to fetch|load failed/i.test(e.message))) {
|
if (e instanceof TypeError || (e instanceof Error && /failed to fetch|load failed/i.test(e.message))) {
|
||||||
return new Error(
|
return new Error(
|
||||||
@@ -145,7 +156,7 @@ async function fetchWithTimeout(
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
return await fetch(url, {
|
return await httpFetch()(url, {
|
||||||
...init,
|
...init,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: { ...authHeaders(), ...(init?.headers ?? {}) },
|
headers: { ...authHeaders(), ...(init?.headers ?? {}) },
|
||||||
@@ -978,7 +989,7 @@ export async function loadStrategy(id: number): Promise<StrategyDto | null> {
|
|||||||
|
|
||||||
/** 전략 저장 (id 없으면 신규, 있으면 수정) — 실패 시 Error throw */
|
/** 전략 저장 (id 없으면 신규, 있으면 수정) — 실패 시 Error throw */
|
||||||
export async function saveStrategy(dto: StrategyDto): Promise<StrategyDto> {
|
export async function saveStrategy(dto: StrategyDto): Promise<StrategyDto> {
|
||||||
const res = await fetch(`${API_BASE}/strategies`, {
|
const res = await httpFetch()(`${API_BASE}/strategies`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: authHeaders(),
|
headers: authHeaders(),
|
||||||
body: JSON.stringify(dto),
|
body: JSON.stringify(dto),
|
||||||
@@ -1337,7 +1348,7 @@ export async function repairStrategyTimeframes(strategyId: number): Promise<void
|
|||||||
/** 차트 실시간 파이프라인 pin (STOMP warm-up) */
|
/** 차트 실시간 파이프라인 pin (STOMP warm-up) */
|
||||||
export async function pinCandleWatch(market: string, candleType: string): Promise<void> {
|
export async function pinCandleWatch(market: string, candleType: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fetch(
|
await httpFetch()(
|
||||||
`${API_BASE}/candles/watch?market=${encodeURIComponent(market)}&type=${encodeURIComponent(candleType)}`,
|
`${API_BASE}/candles/watch?market=${encodeURIComponent(market)}&type=${encodeURIComponent(candleType)}`,
|
||||||
{ method: 'POST' },
|
{ method: 'POST' },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ UPDATES_SRC="${UPDATES_SRC:-$ARTIFACT_DIR/updates}"
|
|||||||
UPDATES_DIR="${UPDATES_DIR:-${WORK_TREE:-$ROOT}/frontend/public/desktop/updates}"
|
UPDATES_DIR="${UPDATES_DIR:-${WORK_TREE:-$ROOT}/frontend/public/desktop/updates}"
|
||||||
WEB_STATIC_DIR="${WEB_STATIC_DIR:-$UPDATES_DIR}"
|
WEB_STATIC_DIR="${WEB_STATIC_DIR:-$UPDATES_DIR}"
|
||||||
BASE_URL="${DESKTOP_UPDATE_BASE_URL:-http://exdev.co.kr/desktop/updates}"
|
BASE_URL="${DESKTOP_UPDATE_BASE_URL:-http://exdev.co.kr/desktop/updates}"
|
||||||
|
DESKTOP_RELEASE_DIR="${GC_DESKTOP_APP_RELEASE_DIR:-$ROOT/data/desktop-releases}"
|
||||||
KEY_PATH="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}"
|
KEY_PATH="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}"
|
||||||
DESKTOP_DIR="$ROOT/desktop"
|
DESKTOP_DIR="$ROOT/desktop"
|
||||||
CONF="$DESKTOP_DIR/src-tauri/tauri.conf.json"
|
CONF="$DESKTOP_DIR/src-tauri/tauri.conf.json"
|
||||||
@@ -27,9 +28,14 @@ log "Web static: $WEB_STATIC_DIR"
|
|||||||
|
|
||||||
mkdir -p "$WEB_STATIC_DIR"
|
mkdir -p "$WEB_STATIC_DIR"
|
||||||
|
|
||||||
# 설치 파일(dist-desktop/*.dmg, *setup.exe) 복사
|
# 설치 파일(dist-desktop/*.dmg, *setup.exe) 복사 — nginx static + backend API 디렉터리
|
||||||
if [[ -d "$ARTIFACT_DIR" ]]; then
|
if [[ -d "$ARTIFACT_DIR" ]]; then
|
||||||
find "$ARTIFACT_DIR" -maxdepth 1 -type f \( -name '*.dmg' -o -name '*setup.exe' \) -exec cp -f {} "$WEB_STATIC_DIR/" \;
|
mkdir -p "$DESKTOP_RELEASE_DIR"
|
||||||
|
find "$ARTIFACT_DIR" -maxdepth 1 -type f \( -name '*.dmg' -o -name '*setup.exe' \) | while read -r f; do
|
||||||
|
cp -f "$f" "$WEB_STATIC_DIR/"
|
||||||
|
cp -f "$f" "$DESKTOP_RELEASE_DIR/"
|
||||||
|
log "Installer: $(basename "$f")"
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PUBLISH_UPDATES="$WEB_STATIC_DIR"
|
PUBLISH_UPDATES="$WEB_STATIC_DIR"
|
||||||
|
|||||||
Reference in New Issue
Block a user