Merge pull request #142 from ragestudio/dev

This commit is contained in:
srgooglo 2025-05-15 15:06:27 +02:00 committed by GitHub
commit 6e6f2c5d86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 379 additions and 679 deletions

@ -1 +1 @@
Subproject commit 94c8d7383e84a2de4b193d27adfcb1baa4163f68
Subproject commit fe5cb1a9a275444abd7307bbd8a4e7c68221369b

View File

@ -1,76 +1,78 @@
{
"defaultVars": {
"fontScale": "1",
"backgroundBlur": "10px",
"colorPrimary": "#ff6064",
"app-color": "#ff6064",
"danger-color": "#ff6064",
"backgroundColorTransparency": "0.8",
"backgroundImage": "",
"fontFamily": "'Noto Sans', sans-serif",
"layoutMargin": "unset",
"layoutPadding": "20px 20px 20px 20px",
"borderRadius": "8px",
"text-color-white": "#d2d2d2",
"text-color-black": "#000000",
"page-transition-duration": "150ms",
"backgroundSize": "cover",
"backgroundPosition": "center",
"backgroundRepeat": "no-repeat",
"backgroundAttachment": "fixed",
"top-bar-height": "52px",
"bottom-bar-height": "80px",
"compact-mode": false
},
"defaultVariant": "dark",
"variants": {
"light": {
"bg_color_1": "255, 255, 255",
"bg_color_2": "200, 200, 200",
"bg_color_3": "150, 150, 150",
"bg_color_4": "100, 100, 100",
"bg_color_5": "45, 45, 45",
"bg_color_6": "0, 0, 0",
"text-color": "var(--text-color-black)",
"svg-color": "var(--text-color)",
"disabled-color": "#d2d2d2",
"layoutBackgroundColor": "255, 255, 255",
"layout-background-contrast": "38, 38, 38",
"background-color-primary": "#ffffff",
"background-color-primary-2": "#f0f0f0",
"shadow-color": "#4b4b4b7c",
"background-color-accent-values": "240, 242, 245",
"background-color-accent": "#f0f2f5",
"background-color-contrast": "#4b4b4b",
"border-color": "rgba(75, 75, 75, 0.2)",
"sidebar-background-color": "var(--background-color-accent)",
"sidedrawer-background-color": "var(--background-color-accent)"
},
"dark": {
"bg_color_1": "38, 38, 38",
"bg_color_2": "65, 65, 65",
"bg_color_3": "90, 90, 90",
"bg_color_4": "115, 115, 115",
"bg_color_5": "140, 140, 140",
"bg_color_6": "165, 165, 165",
"text-color": "var(--text-color-white)",
"svg-color": "var(--text-color)",
"disabled-color": "#4b4b4b",
"shadow-color": "#1010107c",
"layoutBackgroundColor": "38, 38, 38",
"layout-background-contrast": "255, 255, 255",
"background-color-primary": "#262626",
"background-color-primary-2": "#2c2c2c",
"background-color-accent-values": "53, 53, 53",
"background-color-accent": "#353535",
"background-color-contrast": "#ffffff",
"background_disabled": "#0A0A0A",
"border-color": "rgba(170, 170, 170, 0.2)",
"header-text-color": "#d2d2d2",
"button-background-color": "var(--colorPrimary)",
"button-text-color": "var(--background-color-contrast)",
"sidebar-background-color": "var(--background-color-accent)",
"sidedrawer-background-color": "var(--background-color-accent)"
}
}
"defaultVars": {
"fontScale": "1",
"backgroundBlur": "10px",
"colorPrimary": "#ff6064",
"app-color": "#ff6064",
"danger-color": "#ff6064",
"backgroundColorTransparency": "0.8",
"backgroundImage": "",
"fontFamily": "'Noto Sans', sans-serif",
"layoutMargin": "unset",
"layoutPadding": "20px 20px 20px 20px",
"borderRadius": "8px",
"text-color-white": "#d2d2d2",
"text-color-black": "#000000",
"page-transition-duration": "150ms",
"backgroundSize": "cover",
"backgroundPosition": "center",
"backgroundRepeat": "no-repeat",
"backgroundAttachment": "fixed",
"top-bar-height": "52px",
"bottom-bar-height": "80px",
"compact-mode": false
},
"defaultVariant": "dark",
"variants": {
"light": {
"bg_color_1": "255, 255, 255",
"bg_color_2": "200, 200, 200",
"bg_color_3": "150, 150, 150",
"bg_color_4": "100, 100, 100",
"bg_color_5": "45, 45, 45",
"bg_color_6": "0, 0, 0",
"text-color": "var(--text-color-black)",
"svg-color": "var(--text-color)",
"disabled-color": "#d2d2d2",
"layoutBackgroundColor": "255, 255, 255",
"layout-background-contrast": "38, 38, 38",
"background-color-primary": "#ffffff",
"background-color-primary-2": "#f0f0f0",
"shadow-color": "#4b4b4b7c",
"background-color-accent-values": "240, 242, 245",
"background-color-accent": "#f0f2f5",
"background-color-contrast": "#4b4b4b",
"border-color": "rgba(75, 75, 75, 0.2)",
"border-color-solid": "#b3b3b3",
"sidebar-background-color": "var(--background-color-accent)",
"sidedrawer-background-color": "var(--background-color-accent)"
},
"dark": {
"bg_color_1": "38, 38, 38",
"bg_color_2": "65, 65, 65",
"bg_color_3": "90, 90, 90",
"bg_color_4": "115, 115, 115",
"bg_color_5": "140, 140, 140",
"bg_color_6": "165, 165, 165",
"text-color": "var(--text-color-white)",
"svg-color": "var(--text-color)",
"disabled-color": "#4b4b4b",
"shadow-color": "#1010107c",
"layoutBackgroundColor": "38, 38, 38",
"layout-background-contrast": "255, 255, 255",
"background-color-primary": "#262626",
"background-color-primary-2": "#2c2c2c",
"background-color-accent-values": "53, 53, 53",
"background-color-accent": "#353535",
"background-color-contrast": "#ffffff",
"background_disabled": "#0A0A0A",
"border-color": "rgba(170, 170, 170, 0.2)",
"border-color-solid": "#4c4c4c",
"header-text-color": "#d2d2d2",
"button-background-color": "var(--colorPrimary)",
"button-text-color": "var(--background-color-contrast)",
"sidebar-background-color": "var(--background-color-accent)",
"sidedrawer-background-color": "var(--background-color-accent)"
}
}
}

View File

@ -0,0 +1,17 @@
import js from "@eslint/js"
import globals from "globals"
import pluginReact from "eslint-plugin-react"
import { defineConfig } from "eslint/config"
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,jsx}"],
plugins: { js },
extends: ["js/recommended"],
},
{
files: ["**/*.{js,mjs,cjs,jsx}"],
languageOptions: { globals: globals.browser },
},
pluginReact.configs.flat.recommended,
])

View File

@ -1,6 +1,6 @@
{
"name": "@comty/app",
"version": "1.41.0@alpha",
"version": "1.42.0@alpha",
"license": "ComtyLicense",
"main": "electron/main",
"type": "module",
@ -11,7 +11,8 @@
"build": "vite build",
"preview": "vite preview",
"release": "node ./scripts/release.js",
"postinstall": "./scripts/postinstall.sh"
"postinstall": "./scripts/postinstall.sh",
"eslint": "eslint"
},
"dependencies": {
"@ant-design/icons": "^5.4.0",
@ -33,7 +34,7 @@
"axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0",
"classnames": "2.3.1",
"comty.js": "^0.65.5",
"comty.js": "^0.66.0",
"d3": "^7.9.0",
"dashjs": "^5.0.0",
"dompurify": "^3.0.0",
@ -79,9 +80,13 @@
"vite": "^6.2.6"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@octokit/rest": "^21.1.1",
"7zip-min": "1.4.3",
"dotenv": "16.0.3",
"form-data": "^4.0.0"
"eslint": "^9.26.0",
"eslint-plugin-react": "^7.37.5",
"form-data": "^4.0.0",
"globals": "^16.1.0"
}
}

View File

@ -1,237 +0,0 @@
import { EventBus } from "@ragestudio/vessel"
export default class ChunkedUpload {
constructor(params) {
const {
endpoint,
file,
headers = {},
splitChunkSize = 1024 * 1024 * 10,
maxRetries = 3,
delayBeforeRetry = 5,
} = params
if (!endpoint) {
throw new Error("Missing endpoint")
}
if ((!file) instanceof File) {
throw new Error("Invalid or missing file")
}
if (typeof headers !== "object") {
throw new Error("Invalid headers")
}
if (splitChunkSize <= 0) {
throw new Error("Invalid splitChunkSize")
}
this.chunkCount = 0
this.retriesCount = 0
this.splitChunkSize = splitChunkSize
this.totalChunks = Math.ceil(file.size / splitChunkSize)
this.maxRetries = maxRetries
this.delayBeforeRetry = delayBeforeRetry
this.offline = this.paused = false
this.endpoint = endpoint
this.file = file
this.headers = {
...headers,
"uploader-original-name": encodeURIComponent(file.name),
"uploader-file-id": this.getFileUID(file),
"uploader-chunks-total": this.totalChunks,
"chunk-size": splitChunkSize,
"cache-control": "no-cache",
connection: "keep-alive",
}
this.setupListeners()
this.nextSend()
console.debug("[Uploader] Created", {
splitChunkSize: splitChunkSize,
totalChunks: this.totalChunks,
totalSize: file.size,
fileName: file.name,
fileType: file.type,
})
}
_reader = new FileReader()
events = new EventBus()
setupListeners() {
window.addEventListener(
"online",
() =>
!this.offline &&
((this.offline = false),
this.events.emit("online"),
this.nextSend()),
)
window.addEventListener(
"offline",
() => ((this.offline = true), this.events.emit("offline")),
)
}
getFileUID(file) {
return (
Math.floor(Math.random() * 100000000) +
Date.now() +
file.size +
"_tmp"
)
}
loadChunk() {
return new Promise((resolve) => {
const start = this.chunkCount * this.splitChunkSize
const end = Math.min(start + this.splitChunkSize, this.file.size)
this._reader.onload = () => {
resolve(
new Blob([this._reader.result], {
type: "application/octet-stream",
}),
)
}
this._reader.readAsArrayBuffer(this.file.slice(start, end))
})
}
async sendChunk() {
console.log(`[UPLOADER] Sending chunk ${this.chunkCount}`, {
currentChunk: this.chunkCount,
totalChunks: this.totalChunks,
chunk: this.chunk,
})
try {
const res = await fetch(this.endpoint, {
method: "POST",
headers: {
...this.headers,
"uploader-chunk-number": this.chunkCount,
},
body: this.chunk,
})
return res
} catch (error) {
this.manageRetries()
}
}
manageRetries() {
if (++this.retriesCount < this.maxRetries) {
setTimeout(() => this.nextSend(), this.delayBeforeRetry * 1000)
this.events.emit("fileRetry", {
message: `Retrying chunk ${this.chunkCount}`,
chunk: this.chunkCount,
retriesLeft: this.retries - this.retriesCount,
})
} else {
this.events.emit("error", {
message: `No more retries for chunk ${this.chunkCount}`,
})
}
}
async nextSend() {
if (this.paused || this.offline) {
return null
}
this.chunk = await this.loadChunk()
try {
const res = await this.sendChunk()
if (![200, 201, 204].includes(res.status)) {
// failed!!
return this.manageRetries()
}
const data = await res.json()
console.log(`[UPLOADER] Chunk ${this.chunkCount} sent`)
this.chunkCount = this.chunkCount + 1
if (this.chunkCount < this.totalChunks) {
this.nextSend()
}
// check if is the last chunk, if so, handle sse events
if (this.chunkCount === this.totalChunks) {
if (data.sseChannelId || data.sseUrl) {
this.waitOnSSE(data)
} else {
this.events.emit("finish", data)
}
}
this.events.emit("progress", {
percent: Math.round((100 / this.totalChunks) * this.chunkCount),
state: "Uploading",
})
} catch (error) {
this.events.emit("error", error)
}
}
togglePause() {
this.paused = !this.paused
if (!this.paused) {
return this.nextSend()
}
}
waitOnSSE(data) {
// temporal solution until comty.js manages this
const url = `${app.cores.api.client().mainOrigin}/upload/sse_events/${data.sseChannelId}`
console.log(`[UPLOADER] Connecting to SSE channel >`, url)
const eventSource = new EventSource(url)
eventSource.onerror = (error) => {
this.events.emit("error", error)
eventSource.close()
}
eventSource.onopen = () => {
console.log(`[UPLOADER] SSE channel opened`)
}
eventSource.onmessage = (event) => {
// parse json
const messageData = JSON.parse(event.data)
console.log(`[UPLOADER] SSE Event >`, messageData)
if (messageData.event === "done") {
this.events.emit("finish", messageData.result)
eventSource.close()
}
if (messageData.event === "error") {
this.events.emit("error", messageData.result)
eventSource.close()
}
if (messageData.state) {
this.events.emit("progress", {
percent: messageData.percent,
state: messageData.state,
})
}
}
}
}

View File

@ -1,9 +1,10 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { DragDropContext, Droppable } from "react-beautiful-dnd"
import { createSwapy } from "swapy"
import queuedUploadFile from "@utils/queuedUploadFile"
import FilesModel from "@models/files"
import TrackManifest from "@cores/player/classes/TrackManifest"
import { Icons } from "@components/Icons"
@ -209,14 +210,14 @@ class TracksManager extends React.Component {
console.log(
`[${trackManifest.uid}] Founded cover, uploading...`,
)
const coverFile = new File(
[trackManifest._coverBlob],
"cover.jpg",
{ type: trackManifest._coverBlob.type },
)
const coverUpload =
await app.cores.remoteStorage.uploadFile(coverFile)
const coverUpload = await FilesModel.upload(coverFile)
trackManifest.cover = coverUpload.url
}
@ -243,25 +244,16 @@ class TracksManager extends React.Component {
}
uploadToStorage = async (req) => {
const response = await app.cores.remoteStorage
.uploadFile(req.file, {
onProgress: this.handleTrackFileUploadProgress,
headers: {
transformations: "a-dash",
},
})
.catch((error) => {
console.error(error)
antd.message.error(error)
req.onError(error)
return false
})
if (response) {
req.onSuccess(response)
}
await queuedUploadFile(req.file, {
onFinish: (file, response) => {
req.onSuccess(response)
},
onError: req.onError,
onProgress: this.handleTrackFileUploadProgress,
headers: {
transformations: "a-dash",
},
})
}
handleTrackFileUploadProgress = async (file, progress) => {

View File

@ -10,6 +10,7 @@ import { Icons } from "@components/Icons"
import Poll from "@components/Poll"
import clipboardEventFileToFile from "@utils/clipboardEventFileToFile"
import queuedUploadFile from "@utils/queuedUploadFile"
import PostModel from "@models/post"
import SearchModel from "@models/search"
@ -195,22 +196,14 @@ export default class PostCreator extends React.Component {
uploadFile = async (req) => {
this.toggleUploaderVisibility(false)
const request = await app.cores.remoteStorage
.uploadFile(req.file)
.catch((error) => {
console.error(error)
antd.message.error(error)
req.onError(error)
return false
})
if (request) {
console.log(`Upload done >`, request)
return req.onSuccess(request)
}
await queuedUploadFile(req.file, {
onFinish: (file, response) => {
req.onSuccess(response)
},
onError: (file, response) => {
req.onError(response)
},
})
}
removeAttachment = (file_uid) => {

View File

@ -1,12 +1,13 @@
import React from "react"
import { Upload, Progress } from "antd"
import classnames from "classnames"
import queuedUploadFile from "@utils/queuedUploadFile"
import { Icons } from "@components/Icons"
import "./index.less"
export default (props) => {
const UploadButton = (props) => {
const [uploading, setUploading] = React.useState(false)
const [progress, setProgress] = React.useState(null)
@ -40,17 +41,7 @@ export default (props) => {
handleOnStart(req.file.uid, req.file)
await app.cores.remoteStorage.uploadFile(req.file, {
headers: props.headers,
onProgress: (file, progress) => {
setProgress(progress)
handleOnProgress(file.uid, progress)
},
onError: (file, error) => {
setProgress(null)
handleOnError(file.uid, error)
setUploading(false)
},
await queuedUploadFile(req.file, {
onFinish: (file, response) => {
if (typeof props.ctx?.onUpdateItem === "function") {
props.ctx.onUpdateItem(response.url)
@ -67,6 +58,16 @@ export default (props) => {
setProgress(null)
}, 1000)
},
onError: (file, error) => {
setProgress(null)
handleOnError(file.uid, error)
setUploading(false)
},
onProgress: (file, progress) => {
setProgress(progress)
handleOnProgress(file.uid, progress)
},
headers: props.headers,
})
}
@ -106,3 +107,5 @@ export default (props) => {
</Upload>
)
}
export default UploadButton

View File

@ -1,117 +0,0 @@
import { Core } from "@ragestudio/vessel"
import ChunkedUpload from "@classes/ChunkedUpload"
import SessionModel from "@models/session"
export default class RemoteStorage extends Core {
static namespace = "remoteStorage"
static depends = ["api", "tasksQueue"]
public = {
uploadFile: this.uploadFile,
getFileHash: this.getFileHash,
binaryArrayToFile: this.binaryArrayToFile,
}
binaryArrayToFile(bin, filename) {
const { format, data } = bin
const filenameExt = format.split("/")[1]
filename = `${filename}.${filenameExt}`
const byteArray = new Uint8Array(data)
const blob = new Blob([byteArray], { type: data.type })
return new File([blob], filename, {
type: format,
})
}
async getFileHash(file) {
const buffer = await file.arrayBuffer()
const hash = await crypto.subtle.digest("SHA-256", buffer)
const hashArray = Array.from(new Uint8Array(hash))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
return hashHex
}
async uploadFile(
file,
{
onProgress = () => {},
onFinish = () => {},
onError = () => {},
service = "standard",
headers = {},
} = {},
) {
return await new Promise((_resolve, _reject) => {
const fn = async () =>
new Promise((resolve, reject) => {
const uploader = new ChunkedUpload({
endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
splitChunkSize: 5 * 1024 * 1024,
file: file,
service: service,
headers: {
...headers,
"provider-type": service,
Authorization: `Bearer ${SessionModel.token}`,
},
})
uploader.events.on("error", ({ message }) => {
this.console.error("[Uploader] Error", message)
app.cores.notifications.new(
{
title: "Could not upload file",
description: message,
},
{
type: "error",
},
)
if (typeof onError === "function") {
onError(file, message)
}
reject(message)
_reject(message)
})
uploader.events.on("progress", (data) => {
if (typeof onProgress === "function") {
onProgress(file, data)
}
})
uploader.events.on("finish", (data) => {
this.console.debug("[Uploader] Finish", data)
app.cores.notifications.new(
{
title: "File uploaded",
},
{
type: "success",
},
)
if (typeof onFinish === "function") {
onFinish(file, data)
}
resolve(data)
_resolve(data)
})
})
app.cores.tasksQueue.appendToQueue(`upload_${file.name}`, fn)
})
}
}

View File

@ -1,34 +0,0 @@
import { Core } from "@ragestudio/vessel"
import SyncModel from "comty.js/models/sync"
export default class SyncCore extends Core {
static namespace = "sync"
static dependencies = ["api", "settings"]
activeLinkedServices = {}
services = {
}
public = {
getActiveLinkedServices: function () {
return this.activeLinkedServices
}.bind(this),
services: this.services,
}
events = {
"app.initialization.start": async () => {
const activeServices = await SyncModel.getLinkedServices().catch((error) => {
this.console.error(error)
return null
})
if (activeServices) {
this.console.log(`Active services`, activeServices)
this.activeLinkedServices = activeServices
}
}
}
}

View File

@ -2,90 +2,95 @@ import { Core } from "@ragestudio/vessel"
import { Observable } from "object-observer"
export default class TasksQueue extends Core {
static depends = ["settings"]
static depends = ["settings"]
static namespace = "tasksQueue"
static namespace = "tasksQueue"
static get maxRunningTasks() {
return app.cores.settings.get("tasks.maxRunningTasks") ?? 3
}
static get maxRunningTasks() {
return app.cores.settings.get("tasks.maxRunningTasks") ?? 3
}
public = {
appendToQueue: this.appendToQueue.bind(this),
processTasks: this.processTasks.bind(this),
}
public = {
appendToQueue: this.appendToQueue.bind(this),
processTasks: this.processTasks.bind(this),
}
taskQueue = Observable.from([])
taskQueue = Observable.from([])
runningTasksIds = Observable.from([])
runningTasksIds = Observable.from([])
processTasks() {
if (this.runningTasksIds.length >= TasksQueue.maxRunningTasks ?? 1) {
this.console.log("We are already running the maximum number of tasks")
return false
}
processTasks() {
if (this.runningTasksIds.length >= TasksQueue.maxRunningTasks ?? 1) {
this.console.log(
"We are already running the maximum number of tasks",
)
return false
}
// check if there are new tasks in the queue and move them to the tasks array with the maximum number of tasks can be run
if (this.taskQueue.length === 0) {
this.console.log("No tasks in the queue")
return false
}
// check if there are new tasks in the queue and move them to the tasks array with the maximum number of tasks can be run
if (this.taskQueue.length === 0) {
this.console.log("No tasks in the queue")
return false
}
let tasks = this.taskQueue.splice(0, TasksQueue.maxRunningTasks ?? 1)
let tasks = this.taskQueue.splice(0, TasksQueue.maxRunningTasks ?? 1)
tasks = tasks.filter((task) => task)
tasks = tasks.filter((task) => task)
const promises = tasks.map(async (task) => {
if (typeof task.fn !== "function") {
throw new Error("Task must be a function")
}
const promises = tasks.map(async (task) => {
if (typeof task.fn !== "function") {
throw new Error("Task must be a function")
}
if (typeof task.id === "undefined") {
throw new Error("Task id is required")
}
if (typeof task.id === "undefined") {
throw new Error("Task id is required")
}
// add the task to the running tasks array
this.runningTasksIds.push(task.id)
// add the task to the running tasks array
this.runningTasksIds.push(task.id)
const taskResult = await task.fn()
.catch((error) => {
// delete the task from the running tasks array
this.runningTasksIds = this.runningTasksIds.filter((runningTaskId) => runningTaskId !== task.id)
const taskResult = await task.fn().catch((error) => {
// delete the task from the running tasks array
this.runningTasksIds = this.runningTasksIds.filter(
(runningTaskId) => runningTaskId !== task.id,
)
// propagate the error through an exception
throw error
})
// propagate the error through an exception
throw error
})
// delete the task from the running tasks array
this.runningTasksIds = this.runningTasksIds.filter((runningTaskId) => runningTaskId !== task.id)
// delete the task from the running tasks array
this.runningTasksIds = this.runningTasksIds.filter(
(runningTaskId) => runningTaskId !== task.id,
)
return taskResult
})
return taskResult
})
Promise.all(promises)
.then((res) => {
this.processTasks()
})
.catch((error) => {
this.console.error(error)
this.processTasks()
})
}
Promise.all(promises)
.then((res) => {
this.processTasks()
})
.catch((error) => {
this.console.error(error)
this.processTasks()
})
}
appendToQueue(taskId, task) {
if (!taskId) {
throw new Error("Task id is required")
}
appendToQueue(taskId, task) {
if (!taskId) {
throw new Error("Task id is required")
}
if (Array.isArray(task)) {
throw new Error("Task must be a function")
}
if (Array.isArray(task)) {
throw new Error("Task must be a function")
}
this.taskQueue.unshift({
id: taskId,
fn: task,
})
this.taskQueue.unshift({
id: taskId,
fn: task,
})
this.processTasks()
}
this.processTasks()
}
}

View File

@ -54,9 +54,9 @@ const AuthPage = (props) => {
<GradientSVG />
</div>
<div className="wrapper">
<div className="login-page-card">
<div
className="wrapper_background"
className="login-page-card__background"
style={{
backgroundImage: randomWallpaperURL
? `url(${randomWallpaperURL})`
@ -65,7 +65,7 @@ const AuthPage = (props) => {
}}
/>
<div className="content">
<div className="login-page-card__content">
{React.createElement(
keyToComponents[activeKey] ??
keyToComponents["selector"],

View File

@ -1,137 +1,162 @@
.login-page {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: center;
align-items: center;
align-items: center;
width: 100vw;
height: 100vh;
width: 100vw;
height: 100vh;
font-family: "Space Grotesk", sans-serif;
font-family: "Space Grotesk", sans-serif;
.background {
position: absolute;
.background {
position: absolute;
z-index: 10;
z-index: 10;
top: 0;
left: 0;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
width: 100vw;
height: 100vh;
svg {
margin: 0;
svg {
margin: 0;
opacity: 0;
opacity: 0;
animation: opacityIn 3s ease-in-out 0s forwards;
}
animation: opacityIn 3s ease-in-out 0s forwards;
}
background-image: radial-gradient(rgba(var(--layout-background-contrast), 0.3) 1px, transparent 0),
radial-gradient(rgba(var(--layout-background-contrast), 0.3) 1px, transparent 0);
background-image:
radial-gradient(
rgba(var(--layout-background-contrast), 0.3) 1px,
transparent 0
),
radial-gradient(
rgba(var(--layout-background-contrast), 0.3) 1px,
transparent 0
);
background-position: 0 0,
25px 25px;
background-size: 50px 50px;
}
background-position:
0 0,
25px 25px;
background-size: 50px 50px;
}
}
.wrapper {
position: relative;
z-index: 15;
@login-page-card-img-width: 250px;
display: flex;
flex-direction: row;
.login-page-card {
position: relative;
z-index: 15;
width: 55vw;
min-width: 700px;
max-width: 800px;
display: flex;
flex-direction: row;
overflow: hidden;
width: 55vw;
min-width: 700px;
max-width: 800px;
height: 50vh;
min-height: 500px;
max-height: 500px;
height: 50vh;
min-height: 500px;
max-height: 500px;
transition: all 250ms ease-in-out;
background-color: var(--background-color-accent);
border: 3px solid var(--border-color);
background-color: var(--background-color-accent);
border-radius: 12px;
border: 1px solid var(--border-color);
transition: all 250ms ease-in-out;
border-radius: 12px;
overflow: hidden;
.wrapper_background {
height: 100%;
&__background {
position: absolute;
min-width: 250px;
width: 250px;
left: 0;
top: 0;
border-radius: 12px;
height: 100%;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
width: calc(@login-page-card-img-width + 10px);
min-width: calc(@login-page-card-img-width + 10px);
.content {
display: flex;
flex-direction: column;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
align-items: center;
justify-content: center;
z-index: 1;
}
width: 100%;
&__content {
position: absolute;
display: flex;
flex-direction: column;
min-width: 420px;
right: 0;
gap: 20px;
align-items: center;
justify-content: center;
padding: 40px;
width: calc(100% - @login-page-card-img-width);
min-width: 420px;
.content_header {
height: 70px;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
gap: 20px;
padding: 40px;
.actions {
display: flex;
flex-direction: column;
outline: 3px solid var(--border-color-solid);
border-radius: 12px;
align-items: center;
justify-content: center;
background-color: var(--background-color-accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
width: 100%;
z-index: 2;
gap: 15px;
.content_header {
height: 70px;
.ant-btn {
width: 100%;
img {
width: 100%;
height: 100%;
}
}
font-weight: 500;
letter-spacing: -0.6px;
.actions {
display: flex;
flex-direction: column;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
align-items: center;
justify-content: center;
width: 100%;
gap: 15px;
.ant-btn {
width: 100%;
font-weight: 500;
letter-spacing: -0.6px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
@keyframes opacityIn {
from {
opacity: 0;
}
from {
opacity: 0;
}
to {
opacity: 1;
}
to {
opacity: 1;
}
}

View File

@ -0,0 +1,46 @@
import FilesModel from "@models/files"
export default (file, options) => {
if (!app.cores.tasksQueue) {
throw new Error("Missing tasksQueue")
}
return app.cores.tasksQueue.appendToQueue(
`upload_${file.name}`,
async () => {
await FilesModel.upload(file, {
...options,
onError: (file, error) => {
app.cores.notifications.new(
{
title: "Could not upload file",
description: error.message,
},
{
type: "error",
},
)
if (typeof options.onError === "function") {
options.onError(file, error)
}
},
onFinish: (file, data) => {
app.cores.notifications.new(
{
title: "File uploaded",
description: `[${file.name}] uploaded successfully!`,
},
{
type: "success",
},
)
if (typeof options.onFinish === "function") {
options.onFinish(file, data)
}
},
})
},
)
}