mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-22 18:33:56 +00:00
feat: migration to scss
This commit is contained in:
@@ -38,6 +38,9 @@ export class UserPreferences {
|
||||
@Column("boolean", { default: false })
|
||||
startMinimized: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
disableNsfwAlert: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import parseTorrent from "parse-torrent";
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { HydraAnalytics } from "@main/services/hydra-analytics";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -90,6 +91,17 @@ const startGameDownload = async (
|
||||
logger.error("Failed to create game download", err);
|
||||
});
|
||||
|
||||
if (uri.startsWith("magnet:")) {
|
||||
try {
|
||||
const { infoHash } = await parseTorrent(payload.uri);
|
||||
if (infoHash) {
|
||||
HydraAnalytics.postDownload(infoHash).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to parse torrent", err);
|
||||
}
|
||||
}
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CreateUserSubscription } from "./migrations/20241015235142_create_user_
|
||||
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
||||
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
||||
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
||||
import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column";
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
@@ -28,6 +29,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
AddBackgroundImageUrl,
|
||||
AddWinePrefixToGame,
|
||||
AddStartMinimizedColumn,
|
||||
AddDisableNsfwAlertColumn,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddDisableNsfwAlertColumn: HydraMigration = {
|
||||
name: "AddDisableNsfwAlertColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.boolean("disableNsfwAlert").notNullable().defaultTo(0);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("disableNsfwAlert");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -102,7 +102,7 @@ export const mergeAchievements = async (
|
||||
);
|
||||
});
|
||||
})
|
||||
.filter((achievement) => achievement)
|
||||
.filter((achievement) => Boolean(achievement))
|
||||
.map((achievement) => {
|
||||
return {
|
||||
displayName: achievement!.displayName,
|
||||
|
||||
34
src/main/services/hydra-analytics.ts
Normal file
34
src/main/services/hydra-analytics.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { userSubscriptionRepository } from "@main/repository";
|
||||
import axios from "axios";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class HydraAnalytics {
|
||||
private static instance = axios.create({
|
||||
baseURL: import.meta.env.MAIN_VITE_ANALYTICS_API_URL,
|
||||
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
||||
});
|
||||
|
||||
private static async hasActiveSubscription() {
|
||||
const userSubscription = await userSubscriptionRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return (
|
||||
userSubscription?.expiresAt && userSubscription.expiresAt > new Date()
|
||||
);
|
||||
}
|
||||
|
||||
static async postDownload(hash: string) {
|
||||
const hasSubscription = await this.hasActiveSubscription();
|
||||
|
||||
return this.instance
|
||||
.post("/track", {
|
||||
event: "download",
|
||||
attributes: {
|
||||
hash,
|
||||
hasSubscription,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Notification, app, nativeImage } from "electron";
|
||||
import { Notification, app } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { parseICO } from "icojs";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { Game } from "@main/entity";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import fs from "node:fs";
|
||||
import axios from "axios";
|
||||
import path from "node:path";
|
||||
@@ -11,37 +10,38 @@ import sound from "sound-play";
|
||||
import { achievementSoundPath } from "@main/constants";
|
||||
import icon from "@resources/icon.png?asset";
|
||||
import { NotificationOptions, toXmlString } from "./xml";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const getGameIconNativeImage = async (gameId: number) => {
|
||||
try {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
id: gameId,
|
||||
},
|
||||
async function downloadImage(url: string | null) {
|
||||
if (!url) return undefined;
|
||||
if (!url.startsWith("http")) return undefined;
|
||||
|
||||
const fileName = url.split("/").pop()!;
|
||||
const outputPath = path.join(app.getPath("temp"), fileName);
|
||||
const writer = fs.createWriteStream(outputPath);
|
||||
|
||||
const response = await axios.get(url, {
|
||||
responseType: "stream",
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise<string | undefined>((resolve) => {
|
||||
writer.on("finish", () => {
|
||||
resolve(outputPath);
|
||||
});
|
||||
|
||||
if (!game?.iconUrl) return undefined;
|
||||
|
||||
const images = await parseICO(
|
||||
Buffer.from(game.iconUrl.split("base64,")[1], "base64")
|
||||
);
|
||||
|
||||
const highResIcon = images.find((image) => image.width >= 128);
|
||||
if (!highResIcon) return undefined;
|
||||
|
||||
return nativeImage.createFromBuffer(Buffer.from(highResIcon.buffer));
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
writer.on("error", () => {
|
||||
logger.error("Failed to download image", { url });
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const icon = await getGameIconNativeImage(game.id);
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
@@ -51,7 +51,7 @@ export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
ns: "notifications",
|
||||
title: game.title,
|
||||
}),
|
||||
icon,
|
||||
icon: await downloadImage(game.iconUrl),
|
||||
}).show();
|
||||
}
|
||||
};
|
||||
@@ -73,28 +73,6 @@ export const publishNotificationUpdateReadyToInstall = async (
|
||||
|
||||
export const publishNewFriendRequestNotification = async () => {};
|
||||
|
||||
async function downloadImage(url: string | null) {
|
||||
if (!url) return null;
|
||||
if (!url.startsWith("http")) return null;
|
||||
|
||||
const fileName = url.split("/").pop()!;
|
||||
const outputPath = path.join(app.getPath("temp"), fileName);
|
||||
const writer = fs.createWriteStream(outputPath);
|
||||
|
||||
const response = await axios.get(url, {
|
||||
responseType: "stream",
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
writer.on("finish", () => {
|
||||
resolve(outputPath);
|
||||
});
|
||||
writer.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
export const publishCombinedNewAchievementNotification = async (
|
||||
achievementCount,
|
||||
gameCount
|
||||
|
||||
@@ -56,6 +56,7 @@ export const getUserData = () => {
|
||||
id: loggedUser.userId,
|
||||
username: "",
|
||||
bio: "",
|
||||
email: null,
|
||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||
subscription: loggedUser.subscription
|
||||
? {
|
||||
|
||||
@@ -85,6 +85,10 @@ export class WindowManager {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
if (details.url.includes("intercom.io")) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"access-control-allow-origin": ["*"],
|
||||
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
||||
|
||||
1
src/main/vite-env.d.ts
vendored
1
src/main/vite-env.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
||||
readonly MAIN_VITE_API_URL: string;
|
||||
readonly MAIN_VITE_ANALYTICS_API_URL: string;
|
||||
readonly MAIN_VITE_AUTH_URL: string;
|
||||
readonly MAIN_VITE_CHECKOUT_URL: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user