initial commit
144
fluxer_app/crates/libfluxcore/Cargo.lock
generated
Normal file
@@ -0,0 +1,144 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libfluxcore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gif",
|
||||
"ruzstd",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ruzstd"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
12
fluxer_app/crates/libfluxcore/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "libfluxcore"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
gif = "0.13"
|
||||
ruzstd = { version = "0.7", default-features = false, features = ["std"] }
|
||||
wasm-bindgen = { version = "0.2", features = ["std"] }
|
||||
35
fluxer_app/crates/libfluxcore/src/gateway.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use ruzstd::StreamingDecoder;
|
||||
use std::io::{Cursor, Read};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn decompress_zstd_frame(input: &[u8]) -> Result<Box<[u8]>, JsValue> {
|
||||
let mut decoder = StreamingDecoder::new(Cursor::new(input))
|
||||
.map_err(|e| JsValue::from_str(&format!("zstd init error: {e}")))?;
|
||||
|
||||
let mut output = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut output)
|
||||
.map_err(|e| JsValue::from_str(&format!("zstd read error: {e}")))?;
|
||||
|
||||
Ok(output.into_boxed_slice())
|
||||
}
|
||||
566
fluxer_app/crates/libfluxcore/src/gif.rs
Normal file
@@ -0,0 +1,566 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use gif::{ColorOutput, DecodeOptions, DisposalMethod, Encoder as GifEncoder, Frame, Repeat};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
enum EncodeError {
|
||||
TooManyColors,
|
||||
Js(JsValue),
|
||||
}
|
||||
|
||||
impl From<JsValue> for EncodeError {
|
||||
fn from(value: JsValue) -> Self {
|
||||
Self::Js(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn crop_and_rotate_gif(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation_deg: u32,
|
||||
resize_width: Option<u32>,
|
||||
resize_height: Option<u32>,
|
||||
) -> Result<Box<[u8]>, JsValue> {
|
||||
match process_gif(
|
||||
input,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotation_deg,
|
||||
resize_width,
|
||||
resize_height,
|
||||
EncoderMode::Palette,
|
||||
) {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(EncodeError::TooManyColors) => process_gif(
|
||||
input,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotation_deg,
|
||||
resize_width,
|
||||
resize_height,
|
||||
EncoderMode::Quantized,
|
||||
)
|
||||
.map_err(|err| match err {
|
||||
EncodeError::Js(js) => js,
|
||||
EncodeError::TooManyColors => {
|
||||
JsValue::from_str("GIF contains more than 256 unique colors")
|
||||
}
|
||||
}),
|
||||
Err(EncodeError::Js(js)) => Err(js),
|
||||
}
|
||||
}
|
||||
|
||||
enum EncoderMode {
|
||||
Palette,
|
||||
Quantized,
|
||||
}
|
||||
|
||||
fn process_gif(
|
||||
input: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation_deg: u32,
|
||||
resize_width: Option<u32>,
|
||||
resize_height: Option<u32>,
|
||||
mode: EncoderMode,
|
||||
) -> Result<Box<[u8]>, EncodeError> {
|
||||
let mut decoder = create_decoder(input)?;
|
||||
|
||||
let screen_width = decoder.width() as u32;
|
||||
let screen_height = decoder.height() as u32;
|
||||
|
||||
let crop_x = x.min(screen_width);
|
||||
let crop_y = y.min(screen_height);
|
||||
let crop_w = width.min(screen_width - crop_x);
|
||||
let crop_h = height.min(screen_height - crop_y);
|
||||
|
||||
if crop_w == 0 || crop_h == 0 {
|
||||
return Err(EncodeError::Js(JsValue::from_str("Crop area is empty")));
|
||||
}
|
||||
|
||||
let rotation = rotation_deg.rem_euclid(360);
|
||||
|
||||
let (base_w, base_h) = match rotation {
|
||||
90 | 270 => (crop_h, crop_w),
|
||||
_ => (crop_w, crop_h),
|
||||
};
|
||||
|
||||
let (target_w, target_h) = match (
|
||||
resize_width.filter(|w| *w > 0),
|
||||
resize_height.filter(|h| *h > 0),
|
||||
) {
|
||||
(Some(w), Some(h)) => (w, h),
|
||||
_ => (base_w, base_h),
|
||||
};
|
||||
|
||||
if target_w == 0 || target_h == 0 {
|
||||
return Err(EncodeError::Js(JsValue::from_str(
|
||||
"Target dimensions are empty",
|
||||
)));
|
||||
}
|
||||
|
||||
if crop_x == 0
|
||||
&& crop_y == 0
|
||||
&& crop_w == screen_width
|
||||
&& crop_h == screen_height
|
||||
&& rotation == 0
|
||||
&& target_w == screen_width
|
||||
&& target_h == screen_height
|
||||
{
|
||||
return Ok(input.to_vec().into_boxed_slice());
|
||||
}
|
||||
|
||||
let mut frame_encoder = FrameEncoder::new(mode, target_w as u16, target_h as u16)?;
|
||||
|
||||
let mut canvas = vec![0u8; (screen_width * screen_height * 4) as usize];
|
||||
let mut previous_canvas: Option<Vec<u8>> = None;
|
||||
|
||||
let mut processed_any = false;
|
||||
const MAX_TOTAL_PIXELS: u64 = 200_000_000;
|
||||
let mut processed_pixels: u64 = 0;
|
||||
|
||||
while let Some(frame) = decoder
|
||||
.read_next_frame()
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("gif read_next_frame: {e}"))))?
|
||||
{
|
||||
processed_any = true;
|
||||
|
||||
if frame.dispose == DisposalMethod::Previous {
|
||||
previous_canvas = Some(canvas.clone());
|
||||
}
|
||||
|
||||
draw_frame_on_canvas(
|
||||
&mut canvas,
|
||||
screen_width,
|
||||
frame.left,
|
||||
frame.top,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.buffer.as_ref(),
|
||||
);
|
||||
|
||||
let (cw, ch) = (crop_w as usize, crop_h as usize);
|
||||
let cropped = crop_rgba(
|
||||
&canvas,
|
||||
screen_width as usize,
|
||||
screen_height as usize,
|
||||
crop_x as usize,
|
||||
crop_y as usize,
|
||||
cw,
|
||||
ch,
|
||||
)?;
|
||||
|
||||
let (rotated, rw, rh) = match rotation {
|
||||
90 => rotate_rgba_90(&cropped, cw, ch),
|
||||
180 => rotate_rgba_180(&cropped, cw, ch),
|
||||
270 => rotate_rgba_270(&cropped, cw, ch),
|
||||
_ => (cropped, cw, ch),
|
||||
};
|
||||
|
||||
let (final_rgba, _fw, _fh) = if target_w as usize != rw || target_h as usize != rh {
|
||||
let resized =
|
||||
resize_rgba_nearest(&rotated, rw, rh, target_w as usize, target_h as usize);
|
||||
(resized, target_w as usize, target_h as usize)
|
||||
} else {
|
||||
(rotated, rw, rh)
|
||||
};
|
||||
|
||||
processed_pixels += (final_rgba.len() / 4) as u64;
|
||||
if processed_pixels > MAX_TOTAL_PIXELS {
|
||||
return Err(EncodeError::Js(JsValue::from_str(
|
||||
"Animated GIF is too large to crop. Try reducing its dimensions or number of frames.",
|
||||
)));
|
||||
}
|
||||
|
||||
frame_encoder.write_frame(final_rgba, frame.delay)?;
|
||||
|
||||
match frame.dispose {
|
||||
DisposalMethod::Background => {
|
||||
clear_rect(
|
||||
&mut canvas,
|
||||
screen_width,
|
||||
frame.left,
|
||||
frame.top,
|
||||
frame.width,
|
||||
frame.height,
|
||||
);
|
||||
}
|
||||
DisposalMethod::Previous => {
|
||||
if let Some(prev) = previous_canvas.take() {
|
||||
canvas = prev;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !processed_any {
|
||||
return Err(EncodeError::Js(JsValue::from_str("GIF has no frames")));
|
||||
}
|
||||
|
||||
frame_encoder.finish()
|
||||
}
|
||||
|
||||
fn draw_frame_on_canvas(
|
||||
canvas: &mut [u8],
|
||||
canvas_width: u32,
|
||||
left: u16,
|
||||
top: u16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
buffer: &[u8],
|
||||
) {
|
||||
let fw = width as usize;
|
||||
let fh = height as usize;
|
||||
let fx = left as usize;
|
||||
let fy = top as usize;
|
||||
let cw = canvas_width as usize;
|
||||
|
||||
for row in 0..fh {
|
||||
let canvas_y = fy + row;
|
||||
let canvas_offset = (canvas_y * cw + fx) * 4;
|
||||
let frame_offset = row * fw * 4;
|
||||
|
||||
let frame_row = &buffer[frame_offset..frame_offset + fw * 4];
|
||||
let canvas_row = &mut canvas[canvas_offset..canvas_offset + fw * 4];
|
||||
|
||||
for i in 0..fw {
|
||||
let pixel_idx = i * 4;
|
||||
let alpha = frame_row[pixel_idx + 3];
|
||||
if alpha > 0 {
|
||||
canvas_row[pixel_idx] = frame_row[pixel_idx];
|
||||
canvas_row[pixel_idx + 1] = frame_row[pixel_idx + 1];
|
||||
canvas_row[pixel_idx + 2] = frame_row[pixel_idx + 2];
|
||||
canvas_row[pixel_idx + 3] = frame_row[pixel_idx + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_rect(canvas: &mut [u8], canvas_width: u32, x: u16, y: u16, w: u16, h: u16) {
|
||||
let cw = canvas_width as usize;
|
||||
let x = x as usize;
|
||||
let y = y as usize;
|
||||
let w = w as usize;
|
||||
let h = h as usize;
|
||||
|
||||
for row in 0..h {
|
||||
let canvas_y = y + row;
|
||||
let offset = (canvas_y * cw + x) * 4;
|
||||
for i in 0..w {
|
||||
let idx = offset + i * 4;
|
||||
canvas[idx] = 0;
|
||||
canvas[idx + 1] = 0;
|
||||
canvas[idx + 2] = 0;
|
||||
canvas[idx + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn crop_rgba(
|
||||
src: &[u8],
|
||||
src_w: usize,
|
||||
src_h: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
if x + w > src_w || y + h > src_h {
|
||||
return Err(JsValue::from_str("Crop rect out of bounds"));
|
||||
}
|
||||
|
||||
let mut dst = vec![0u8; w * h * 4];
|
||||
|
||||
for row in 0..h {
|
||||
let src_y = y + row;
|
||||
let src_offset = (src_y * src_w + x) * 4;
|
||||
let dst_offset = row * w * 4;
|
||||
dst[dst_offset..dst_offset + w * 4].copy_from_slice(&src[src_offset..src_offset + w * 4]);
|
||||
}
|
||||
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
fn rotate_rgba_90(src: &[u8], src_w: usize, src_h: usize) -> (Vec<u8>, usize, usize) {
|
||||
let dst_w = src_h;
|
||||
let dst_h = src_w;
|
||||
let mut dst = vec![0u8; dst_w * dst_h * 4];
|
||||
|
||||
for y in 0..src_h {
|
||||
for x in 0..src_w {
|
||||
let src_idx = (y * src_w + x) * 4;
|
||||
let dst_x = src_h - 1 - y;
|
||||
let dst_y = x;
|
||||
let dst_idx = (dst_y * dst_w + dst_x) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
(dst, dst_w, dst_h)
|
||||
}
|
||||
|
||||
fn rotate_rgba_180(src: &[u8], src_w: usize, src_h: usize) -> (Vec<u8>, usize, usize) {
|
||||
let mut dst = vec![0u8; src.len()];
|
||||
for y in 0..src_h {
|
||||
for x in 0..src_w {
|
||||
let src_idx = (y * src_w + x) * 4;
|
||||
let dst_x = src_w - 1 - x;
|
||||
let dst_y = src_h - 1 - y;
|
||||
let dst_idx = (dst_y * src_w + dst_x) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
(dst, src_w, src_h)
|
||||
}
|
||||
|
||||
fn rotate_rgba_270(src: &[u8], src_w: usize, src_h: usize) -> (Vec<u8>, usize, usize) {
|
||||
let dst_w = src_h;
|
||||
let dst_h = src_w;
|
||||
let mut dst = vec![0u8; dst_w * dst_h * 4];
|
||||
|
||||
for y in 0..src_h {
|
||||
for x in 0..src_w {
|
||||
let src_idx = (y * src_w + x) * 4;
|
||||
let dst_x = y;
|
||||
let dst_y = dst_h - 1 - x;
|
||||
let dst_idx = (dst_y * dst_w + dst_x) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
(dst, dst_w, dst_h)
|
||||
}
|
||||
|
||||
fn resize_rgba_nearest(
|
||||
src: &[u8],
|
||||
src_w: usize,
|
||||
src_h: usize,
|
||||
dst_w: usize,
|
||||
dst_h: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut dst = vec![0u8; dst_w * dst_h * 4];
|
||||
|
||||
for dy in 0..dst_h {
|
||||
let sy = dy * src_h / dst_h;
|
||||
for dx in 0..dst_w {
|
||||
let sx = dx * src_w / dst_w;
|
||||
let src_idx = (sy * src_w + sx) * 4;
|
||||
let dst_idx = (dy * dst_w + dx) * 4;
|
||||
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
dst
|
||||
}
|
||||
|
||||
fn create_decoder(input: &[u8]) -> Result<gif::Decoder<Cursor<&[u8]>>, EncodeError> {
|
||||
let cursor = Cursor::new(input);
|
||||
let mut options = DecodeOptions::new();
|
||||
options.set_color_output(ColorOutput::RGBA);
|
||||
options
|
||||
.read_info(cursor)
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("gif read_info: {e}"))))
|
||||
}
|
||||
|
||||
enum FrameEncoder {
|
||||
Palette(PaletteFrameEncoder),
|
||||
Quantized(QuantizedFrameEncoder),
|
||||
}
|
||||
|
||||
impl FrameEncoder {
|
||||
fn new(mode: EncoderMode, width: u16, height: u16) -> Result<Self, EncodeError> {
|
||||
match mode {
|
||||
EncoderMode::Palette => PaletteFrameEncoder::new(width, height).map(Self::Palette),
|
||||
EncoderMode::Quantized => {
|
||||
QuantizedFrameEncoder::new(width, height).map(Self::Quantized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_frame(&mut self, rgba: Vec<u8>, delay: u16) -> Result<(), EncodeError> {
|
||||
match self {
|
||||
Self::Palette(enc) => enc.write_frame(rgba, delay),
|
||||
Self::Quantized(enc) => enc.write_frame(rgba, delay),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Box<[u8]>, EncodeError> {
|
||||
match self {
|
||||
Self::Palette(enc) => enc.finish(),
|
||||
Self::Quantized(enc) => enc.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteFrameEncoder {
|
||||
encoder: GifEncoder<Cursor<Vec<u8>>>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl PaletteFrameEncoder {
|
||||
fn new(width: u16, height: u16) -> Result<Self, EncodeError> {
|
||||
let cursor = Cursor::new(Vec::new());
|
||||
let mut encoder = GifEncoder::new(cursor, width, height, &[])
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("GifEncoder::new: {e}"))))?;
|
||||
encoder
|
||||
.set_repeat(Repeat::Infinite)
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("set_repeat: {e}"))))?;
|
||||
Ok(Self {
|
||||
encoder,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_frame(&mut self, rgba: Vec<u8>, delay: u16) -> Result<(), EncodeError> {
|
||||
let PaletteFrameData {
|
||||
indices,
|
||||
palette,
|
||||
transparent_index,
|
||||
} = PaletteFrameData::from_rgba(&rgba)?;
|
||||
|
||||
let mut frame = Frame::default();
|
||||
frame.width = self.width;
|
||||
frame.height = self.height;
|
||||
frame.delay = delay;
|
||||
frame.buffer = Cow::Owned(indices);
|
||||
frame.palette = Some(palette);
|
||||
frame.transparent = transparent_index;
|
||||
self.encoder.write_frame(&frame).map_err(map_encoding_error)
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Box<[u8]>, EncodeError> {
|
||||
let cursor = self.encoder.into_inner().map_err(map_io_error)?;
|
||||
Ok(cursor.into_inner().into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
struct PaletteFrameData {
|
||||
indices: Vec<u8>,
|
||||
palette: Vec<u8>,
|
||||
transparent_index: Option<u8>,
|
||||
}
|
||||
|
||||
impl PaletteFrameData {
|
||||
fn from_rgba(rgba: &[u8]) -> Result<Self, EncodeError> {
|
||||
let mut palette = Vec::with_capacity(256 * 3);
|
||||
let mut color_to_index = HashMap::with_capacity(256);
|
||||
let mut transparent_index = None;
|
||||
let mut indices = Vec::with_capacity(rgba.len() / 4);
|
||||
|
||||
for pixel in rgba.chunks_exact(4) {
|
||||
let idx = if pixel[3] == 0 {
|
||||
if let Some(idx) = transparent_index {
|
||||
idx
|
||||
} else {
|
||||
let next_index = palette.len() / 3;
|
||||
if next_index >= 256 {
|
||||
return Err(EncodeError::TooManyColors);
|
||||
}
|
||||
palette.extend_from_slice(&[0, 0, 0]);
|
||||
let idx = next_index as u8;
|
||||
transparent_index = Some(idx);
|
||||
idx
|
||||
}
|
||||
} else {
|
||||
let key = [pixel[0], pixel[1], pixel[2]];
|
||||
if let Some(&idx) = color_to_index.get(&key) {
|
||||
idx
|
||||
} else {
|
||||
let next_index = palette.len() / 3;
|
||||
if next_index >= 256 {
|
||||
return Err(EncodeError::TooManyColors);
|
||||
}
|
||||
palette.extend_from_slice(&key);
|
||||
let idx = next_index as u8;
|
||||
color_to_index.insert(key, idx);
|
||||
idx
|
||||
}
|
||||
};
|
||||
indices.push(idx);
|
||||
}
|
||||
|
||||
if palette.is_empty() {
|
||||
palette.extend_from_slice(&[0, 0, 0]);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
indices,
|
||||
palette,
|
||||
transparent_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct QuantizedFrameEncoder {
|
||||
encoder: GifEncoder<Cursor<Vec<u8>>>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl QuantizedFrameEncoder {
|
||||
fn new(width: u16, height: u16) -> Result<Self, EncodeError> {
|
||||
let cursor = Cursor::new(Vec::new());
|
||||
let mut encoder = GifEncoder::new(cursor, width, height, &[])
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("GifEncoder::new: {e}"))))?;
|
||||
encoder
|
||||
.set_repeat(Repeat::Infinite)
|
||||
.map_err(|e| EncodeError::Js(JsValue::from_str(&format!("set_repeat: {e}"))))?;
|
||||
Ok(Self {
|
||||
encoder,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_frame(&mut self, mut rgba: Vec<u8>, delay: u16) -> Result<(), EncodeError> {
|
||||
let mut frame = Frame::from_rgba_speed(self.width, self.height, &mut rgba, 10);
|
||||
frame.delay = delay;
|
||||
self.encoder.write_frame(&frame).map_err(map_encoding_error)
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Box<[u8]>, EncodeError> {
|
||||
let cursor = self.encoder.into_inner().map_err(map_io_error)?;
|
||||
Ok(cursor.into_inner().into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_encoding_error(err: gif::EncodingError) -> EncodeError {
|
||||
EncodeError::Js(JsValue::from_str(&format!("gif encode: {err}")))
|
||||
}
|
||||
|
||||
fn map_io_error(err: std::io::Error) -> EncodeError {
|
||||
EncodeError::Js(JsValue::from_str(&format!("gif io: {err}")))
|
||||
}
|
||||
24
fluxer_app/crates/libfluxcore/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod gif;
|
||||
pub mod gateway;
|
||||
|
||||
pub use gif::crop_and_rotate_gif;
|
||||
pub use gateway::decompress_zstd_frame;
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Required for Electron on arm64 -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Microphone: need BOTH audio-input (hardened runtime) AND microphone (sandbox) -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
|
||||
<!-- Camera -->
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
|
||||
<!-- Network -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
|
||||
<!-- Passkeys: application identifier and associated domains (restricted entitlements) -->
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>3G5837T29K.app.fluxer.canary</string>
|
||||
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>webcredentials:web.canary.fluxer.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Required for Electron on arm64 -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Microphone: need BOTH audio-input (hardened runtime) AND microphone (sandbox) -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
|
||||
<!-- Camera -->
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
|
||||
<!-- Network -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
|
||||
<!-- NOTE: No restricted entitlements (associated-domains, application-identifier) here.
|
||||
This file is for helper apps/frameworks which don't need passkey capabilities. -->
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Required for Electron on arm64 -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Microphone: need BOTH audio-input (hardened runtime) AND microphone (sandbox) -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
|
||||
<!-- Camera -->
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
|
||||
<!-- Network -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
|
||||
<!-- Passkeys: application identifier and associated domains (restricted entitlements) -->
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>3G5837T29K.app.fluxer</string>
|
||||
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>webcredentials:fluxer.app</string>
|
||||
<string>webcredentials:web.fluxer.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
fluxer_app/electron-build-resources/icons-canary/1024x1024.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/128x128.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/16x16.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
fluxer_app/electron-build-resources/icons-canary/24x24.png
Normal file
|
After Width: | Height: | Size: 865 B |
BIN
fluxer_app/electron-build-resources/icons-canary/256x256.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/48x48.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/512x512.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/64x64.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="633" height="512" viewBox="0 0 633 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M46.3631 210.286C19.4771 210.286 -2.98429 188.554 0.324514 161.87C3.90111 133.034 11.3051 108.486 22.5371 88.228C39.9071 57.448 62.4611 35.048 90.1931 21.028C118.231 7.01002 147.793 0 178.879 0C209.965 0 238.155 7.01002 263.451 21.028C289.051 34.742 318.765 55.772 352.593 84.114C375.451 103.314 393.431 117.028 406.537 125.258C419.945 133.18 434.879 137.142 451.337 137.142C477.851 137.142 498.727 127.696 513.965 108.8C521.231 99.79 526.661 88.77 530.255 75.74C537.627 49.008 558.305 25.6 586.033 25.6C612.817 25.6 635.309 47.078 632.223 73.684C628.861 102.664 621.441 127.322 609.965 147.658C592.593 178.438 569.889 200.838 541.851 214.858C513.813 228.876 484.251 235.886 453.165 235.886C422.079 235.886 393.737 229.334 368.137 216.228C342.537 202.82 312.975 181.334 279.451 151.772C257.507 132.572 239.679 119.01 225.965 111.086C212.251 102.858 197.165 98.742 180.707 98.742C156.021 98.742 135.603 107.276 119.451 124.342C111.043 133.226 104.947 145.246 101.165 160.404C94.4911 187.138 73.9191 210.286 46.3631 210.286ZM46.3631 486.4C19.4771 486.4 -2.98429 464.668 0.324514 437.986C3.90111 409.148 11.3051 384.6 22.5371 364.342C39.9071 333.562 62.4611 311.162 90.1931 297.142C118.231 283.124 147.793 276.114 178.879 276.114C209.965 276.114 238.155 283.124 263.451 297.142C289.051 310.858 318.765 331.886 352.593 360.228C375.451 379.428 393.431 393.142 406.537 401.372C419.945 409.296 434.879 413.258 451.337 413.258C477.851 413.258 498.727 403.81 513.965 384.914C521.231 375.904 526.661 364.884 530.255 351.854C537.627 325.122 558.305 301.714 586.033 301.714C612.817 301.714 635.309 323.194 632.223 349.798C628.861 378.778 621.441 403.436 609.965 423.772C592.593 454.552 569.889 476.952 541.851 490.972C513.813 504.99 484.251 512 453.165 512C422.079 512 393.737 505.448 368.137 492.342C342.537 478.934 312.975 457.448 279.451 427.886C257.507 408.686 239.679 395.124 225.965 387.2C212.251 378.972 197.165 374.858 180.707 374.858C156.021 374.858 135.603 383.39 119.451 400.458C111.043 409.34 104.947 421.36 101.165 436.518C94.4911 463.252 73.9191 486.4 46.3631 486.4Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"fill": {
|
||||
"solid": "display-p3:0.95215,0.66553,0.23462,1.00000"
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"image-name": "Vector.svg",
|
||||
"name": "Vector"
|
||||
}
|
||||
],
|
||||
"shadow": {
|
||||
"kind": "neutral",
|
||||
"opacity": 0.5
|
||||
},
|
||||
"translucency": {
|
||||
"enabled": true,
|
||||
"value": 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms": {
|
||||
"circles": ["watchOS"],
|
||||
"squares": "shared"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 701 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 883 B |
BIN
fluxer_app/electron-build-resources/icons-canary/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 805 B |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/icon.ico
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
fluxer_app/electron-build-resources/icons-canary/icon.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/1024x1024.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/128x128.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/16x16.png
Normal file
|
After Width: | Height: | Size: 676 B |
BIN
fluxer_app/electron-build-resources/icons-stable/24x24.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/256x256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/48x48.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/512x512.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/64x64.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="633" height="512" viewBox="0 0 633 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M46.3631 210.286C19.4771 210.286 -2.98429 188.554 0.324514 161.87C3.90111 133.034 11.3051 108.486 22.5371 88.228C39.9071 57.448 62.4611 35.048 90.1931 21.028C118.231 7.01002 147.793 0 178.879 0C209.965 0 238.155 7.01002 263.451 21.028C289.051 34.742 318.765 55.772 352.593 84.114C375.451 103.314 393.431 117.028 406.537 125.258C419.945 133.18 434.879 137.142 451.337 137.142C477.851 137.142 498.727 127.696 513.965 108.8C521.231 99.79 526.661 88.77 530.255 75.74C537.627 49.008 558.305 25.6 586.033 25.6C612.817 25.6 635.309 47.078 632.223 73.684C628.861 102.664 621.441 127.322 609.965 147.658C592.593 178.438 569.889 200.838 541.851 214.858C513.813 228.876 484.251 235.886 453.165 235.886C422.079 235.886 393.737 229.334 368.137 216.228C342.537 202.82 312.975 181.334 279.451 151.772C257.507 132.572 239.679 119.01 225.965 111.086C212.251 102.858 197.165 98.742 180.707 98.742C156.021 98.742 135.603 107.276 119.451 124.342C111.043 133.226 104.947 145.246 101.165 160.404C94.4911 187.138 73.9191 210.286 46.3631 210.286ZM46.3631 486.4C19.4771 486.4 -2.98429 464.668 0.324514 437.986C3.90111 409.148 11.3051 384.6 22.5371 364.342C39.9071 333.562 62.4611 311.162 90.1931 297.142C118.231 283.124 147.793 276.114 178.879 276.114C209.965 276.114 238.155 283.124 263.451 297.142C289.051 310.858 318.765 331.886 352.593 360.228C375.451 379.428 393.431 393.142 406.537 401.372C419.945 409.296 434.879 413.258 451.337 413.258C477.851 413.258 498.727 403.81 513.965 384.914C521.231 375.904 526.661 364.884 530.255 351.854C537.627 325.122 558.305 301.714 586.033 301.714C612.817 301.714 635.309 323.194 632.223 349.798C628.861 378.778 621.441 403.436 609.965 423.772C592.593 454.552 569.889 476.952 541.851 490.972C513.813 504.99 484.251 512 453.165 512C422.079 512 393.737 505.448 368.137 492.342C342.537 478.934 312.975 457.448 279.451 427.886C257.507 408.686 239.679 395.124 225.965 387.2C212.251 378.972 197.165 374.858 180.707 374.858C156.021 374.858 135.603 383.39 119.451 400.458C111.043 409.34 104.947 421.36 101.165 436.518C94.4911 463.252 73.9191 486.4 46.3631 486.4Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"fill": {
|
||||
"solid": "display-p3:0.27100,0.25562,0.81934,1.00000"
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"fill": "automatic",
|
||||
"image-name": "Vector.svg",
|
||||
"name": "Vector"
|
||||
}
|
||||
],
|
||||
"shadow": {
|
||||
"kind": "neutral",
|
||||
"opacity": 0.5
|
||||
},
|
||||
"translucency": {
|
||||
"enabled": true,
|
||||
"value": 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms": {
|
||||
"circles": ["watchOS"],
|
||||
"squares": "shared"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 807 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 930 B |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/icon.ico
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
fluxer_app/electron-build-resources/icons-stable/icon.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
52
fluxer_app/electron-build-resources/notarize.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const {notarize} = require('@electron/notarize');
|
||||
|
||||
exports.default = async function notarizing(context) {
|
||||
const {electronPlatformName, appOutDir} = context;
|
||||
|
||||
if (electronPlatformName !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
|
||||
console.log('Skipping notarization: Apple credentials not set');
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
const appPath = `${appOutDir}/${appName}.app`;
|
||||
|
||||
console.log(`Notarizing ${appPath}...`);
|
||||
|
||||
try {
|
||||
await notarize({
|
||||
tool: 'notarytool',
|
||||
appPath,
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
||||
teamId: process.env.APPLE_TEAM_ID,
|
||||
});
|
||||
console.log('Notarization complete');
|
||||
} catch (error) {
|
||||
console.error('Notarization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
114
fluxer_app/electron-builder.canary.yaml
Normal file
@@ -0,0 +1,114 @@
|
||||
appId: app.fluxer.canary
|
||||
productName: Fluxer Canary
|
||||
copyright: Copyright (C) 2026 Fluxer Contributors
|
||||
|
||||
artifactName: fluxer-${version}-${arch}.${ext}
|
||||
|
||||
directories:
|
||||
output: dist-electron
|
||||
buildResources: electron-build-resources
|
||||
|
||||
files:
|
||||
- src-electron/dist/**/*
|
||||
- "!**/*.map"
|
||||
- "!**/*.md"
|
||||
- "!**/README*"
|
||||
- "!**/readme*"
|
||||
- "!**/CHANGELOG*"
|
||||
- "!**/LICENSE*"
|
||||
- "!**/.github/**"
|
||||
- "!**/docs/**"
|
||||
- "!**/doc/**"
|
||||
- "!**/example/**"
|
||||
- "!**/examples/**"
|
||||
- "!**/test/**"
|
||||
- "!**/tests/**"
|
||||
- "!**/__tests__/**"
|
||||
- "!**/*.ts"
|
||||
- "!**/tsconfig*.json"
|
||||
|
||||
extraMetadata:
|
||||
main: src-electron/dist/main/index.js
|
||||
type: module
|
||||
|
||||
asar: true
|
||||
compression: normal
|
||||
|
||||
asarUnpack:
|
||||
- "**/*.node"
|
||||
- "**/node_modules/uiohook-napi/**"
|
||||
- "**/node_modules/input-monitoring-check/**"
|
||||
- "**/src-electron/dist/preload/**"
|
||||
|
||||
extraResources:
|
||||
- from: electron-build-resources/icons-canary/512x512.png
|
||||
to: 512x512.png
|
||||
- from: electron-build-resources/icons-canary/badges
|
||||
to: badges
|
||||
- from: electron-build-resources/icons-canary/_compiled/Assets.car
|
||||
to: Assets.car
|
||||
|
||||
mac:
|
||||
category: public.app-category.social-networking
|
||||
icon: electron-build-resources/icons-canary/_compiled/AppIcon.icns
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
entitlements: electron-build-resources/entitlements.mac.canary.plist
|
||||
entitlementsInherit: electron-build-resources/entitlements.mac.inherit.plist
|
||||
provisioningProfile: electron-build-resources/profiles/Fluxer_Canary.provisionprofile
|
||||
extendInfo:
|
||||
CFBundleIconName: AppIcon
|
||||
NSMicrophoneUsageDescription: Fluxer needs access to your microphone for voice chat.
|
||||
NSCameraUsageDescription: Fluxer needs access to your camera for video chat.
|
||||
NSInputMonitoringUsageDescription: Fluxer needs Input Monitoring access for global shortcuts and hotkeys.
|
||||
notarize: true
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
|
||||
dmg:
|
||||
sign: false
|
||||
icon: electron-build-resources/icons-canary/_compiled/AppIcon.icns
|
||||
format: UDZO
|
||||
contents:
|
||||
- x: 130
|
||||
y: 220
|
||||
- x: 410
|
||||
y: 220
|
||||
type: link
|
||||
path: /Applications
|
||||
|
||||
win:
|
||||
icon: electron-build-resources/icons-canary/icon.ico
|
||||
target:
|
||||
- squirrel
|
||||
|
||||
squirrelWindows:
|
||||
iconUrl: https://fluxerstatic.com/web/icons/desktop/canary/icon.ico
|
||||
|
||||
linux:
|
||||
icon: electron-build-resources/icons-canary
|
||||
category: Network
|
||||
maintainer: Fluxer Contributors
|
||||
synopsis: Chat that puts you first (Canary)
|
||||
description: Canary build of Fluxer. Chat that puts you first. Built for friends, groups, and communities.
|
||||
executableName: fluxercanary
|
||||
target:
|
||||
- dir
|
||||
- AppImage
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
mimeTypes:
|
||||
- x-scheme-handler/fluxer
|
||||
|
||||
protocols:
|
||||
- name: Fluxer
|
||||
schemes:
|
||||
- fluxer
|
||||
114
fluxer_app/electron-builder.yaml
Normal file
@@ -0,0 +1,114 @@
|
||||
appId: app.fluxer
|
||||
productName: Fluxer
|
||||
copyright: Copyright (C) 2026 Fluxer Contributors
|
||||
|
||||
artifactName: fluxer-${version}-${arch}.${ext}
|
||||
|
||||
directories:
|
||||
output: dist-electron
|
||||
buildResources: electron-build-resources
|
||||
|
||||
files:
|
||||
- src-electron/dist/**/*
|
||||
- "!**/*.map"
|
||||
- "!**/*.md"
|
||||
- "!**/README*"
|
||||
- "!**/readme*"
|
||||
- "!**/CHANGELOG*"
|
||||
- "!**/LICENSE*"
|
||||
- "!**/.github/**"
|
||||
- "!**/docs/**"
|
||||
- "!**/doc/**"
|
||||
- "!**/example/**"
|
||||
- "!**/examples/**"
|
||||
- "!**/test/**"
|
||||
- "!**/tests/**"
|
||||
- "!**/__tests__/**"
|
||||
- "!**/*.ts"
|
||||
- "!**/tsconfig*.json"
|
||||
|
||||
extraMetadata:
|
||||
main: src-electron/dist/main/index.js
|
||||
type: module
|
||||
|
||||
asar: true
|
||||
compression: normal
|
||||
|
||||
asarUnpack:
|
||||
- "**/*.node"
|
||||
- "**/node_modules/uiohook-napi/**"
|
||||
- "**/node_modules/input-monitoring-check/**"
|
||||
- "**/src-electron/dist/preload/**"
|
||||
|
||||
extraResources:
|
||||
- from: electron-build-resources/icons-stable/512x512.png
|
||||
to: 512x512.png
|
||||
- from: electron-build-resources/icons-stable/badges
|
||||
to: badges
|
||||
- from: electron-build-resources/icons-stable/_compiled/Assets.car
|
||||
to: Assets.car
|
||||
|
||||
mac:
|
||||
category: public.app-category.social-networking
|
||||
icon: electron-build-resources/icons-stable/_compiled/AppIcon.icns
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
entitlements: electron-build-resources/entitlements.mac.stable.plist
|
||||
entitlementsInherit: electron-build-resources/entitlements.mac.inherit.plist
|
||||
provisioningProfile: electron-build-resources/profiles/Fluxer.provisionprofile
|
||||
extendInfo:
|
||||
CFBundleIconName: AppIcon
|
||||
NSMicrophoneUsageDescription: Fluxer needs access to your microphone for voice chat.
|
||||
NSCameraUsageDescription: Fluxer needs access to your camera for video chat.
|
||||
NSInputMonitoringUsageDescription: Fluxer needs Input Monitoring access for global shortcuts and hotkeys.
|
||||
notarize: true
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
|
||||
dmg:
|
||||
sign: false
|
||||
icon: electron-build-resources/icons-stable/_compiled/AppIcon.icns
|
||||
format: UDZO
|
||||
contents:
|
||||
- x: 130
|
||||
y: 220
|
||||
- x: 410
|
||||
y: 220
|
||||
type: link
|
||||
path: /Applications
|
||||
|
||||
win:
|
||||
icon: electron-build-resources/icons-stable/icon.ico
|
||||
target:
|
||||
- squirrel
|
||||
|
||||
squirrelWindows:
|
||||
iconUrl: https://fluxerstatic.com/web/icons/desktop/stable/icon.ico
|
||||
|
||||
linux:
|
||||
icon: electron-build-resources/icons-stable
|
||||
category: Network
|
||||
maintainer: Fluxer Contributors
|
||||
synopsis: Chat that puts you first
|
||||
description: Chat that puts you first. Built for friends, groups, and communities. Text, voice, and video. Open source and community-funded.
|
||||
executableName: fluxer
|
||||
target:
|
||||
- dir
|
||||
- AppImage
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
mimeTypes:
|
||||
- x-scheme-handler/fluxer
|
||||
|
||||
protocols:
|
||||
- name: Fluxer
|
||||
schemes:
|
||||
- fluxer
|
||||
30
fluxer_app/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Fluxer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1, user-scalable=no">
|
||||
<meta name="description" content="Chat that puts you first. Built for friends, groups, and communities. Text, voice, and video. Open source and community-funded.">
|
||||
<link rel="preconnect" href="https://fluxerstatic.com">
|
||||
<link rel="stylesheet" href="https://fluxerstatic.com/fonts/ibm-plex.css">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://fluxerstatic.com/web/favicon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://fluxerstatic.com/web/apple-touch-icon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="Fluxer">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#4641D9">
|
||||
<meta name="msapplication-navbutton-color" content="#4641D9">
|
||||
<meta name="msapplication-TileColor" content="#4641D9">
|
||||
<meta name="msapplication-config" content="https://fluxerstatic.com/web/browserconfig.xml">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="display" content="standalone">
|
||||
<script nonce="{{CSP_NONCE_PLACEHOLDER}}">(function(){try{var loc=window.location;if(loc.pathname==='/'){var target='/channels/@me';if(loc.search)target+=loc.search;if(loc.hash)target+=loc.hash;loc.replace(target);}}catch(e){}})();</script>
|
||||
<script nonce="{{CSP_NONCE_PLACEHOLDER}}">(function(){try{var t=localStorage.getItem('theme');if(t){document.documentElement.classList.add('theme-'+t)}}catch{}})()</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>JavaScript is required to use this application.</noscript>
|
||||
</body>
|
||||
</html>
|
||||
29
fluxer_app/knip.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": ["src-electron/main/index.ts", "scripts/*.mjs"],
|
||||
"ignore": [
|
||||
"lingui.config.js",
|
||||
"postcss.config.js",
|
||||
"src/sw/worker.ts",
|
||||
"pkgs/libfluxcore/libfluxcore_bg.wasm.d.ts",
|
||||
"pkgs/libfluxcore/libfluxcore.d.ts",
|
||||
"src/types/browser.d.ts"
|
||||
],
|
||||
"ignoreDependencies": [
|
||||
"@electron-webauthn/native",
|
||||
"electron-webauthn-mac",
|
||||
"fs-extra",
|
||||
"undici",
|
||||
"update-electron-app",
|
||||
"@types/ws",
|
||||
"electron-builder-squirrel-windows",
|
||||
"esbuild"
|
||||
],
|
||||
"ignoreBinaries": ["go"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"project": ["src/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"exports": "off",
|
||||
"types": "off"
|
||||
}
|
||||
}
|
||||
67
fluxer_app/lingui.config.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
locales: [
|
||||
'ar',
|
||||
'bg',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'es-419',
|
||||
'fi',
|
||||
'fr',
|
||||
'he',
|
||||
'hi',
|
||||
'hr',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'lt',
|
||||
'nl',
|
||||
'no',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv-SE',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
sourceLocale: 'en-US',
|
||||
catalogs: [
|
||||
{
|
||||
path: 'src/locales/{locale}/messages',
|
||||
include: ['src'],
|
||||
exclude: ['**/node_modules/**', '**/*.d.ts'],
|
||||
},
|
||||
],
|
||||
format: 'po',
|
||||
compileNamespace: 'es',
|
||||
};
|
||||
194
fluxer_app/package.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"name": "fluxer_app",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "Fluxer desktop client. Chat that puts you first.",
|
||||
"homepage": "https://fluxer.app",
|
||||
"author": "Fluxer Contributors <developers@fluxer.app>",
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"**/*.css"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm wasm:codegen && pnpm generate:colors && pnpm generate:masks && pnpm generate:css-types && tsc --noEmit && pnpm lingui:compile && rm -rf dist && rspack build --mode production && npx tsx scripts/build-sw.mjs",
|
||||
"dev": "pnpm wasm:codegen && pnpm generate:colors && pnpm generate:masks && pnpm generate:css-types && pnpm lingui:compile && rm -rf dist && rspack serve",
|
||||
"generate:css-types": "tcm src --pattern '**/*.module.css'",
|
||||
"electron:build:canary": "BUILD_CHANNEL=canary pnpm electron:compile && BUILD_CHANNEL=canary electron-builder --config electron-builder.canary.yaml",
|
||||
"electron:build:stable": "pnpm electron:compile && electron-builder --config electron-builder.yaml",
|
||||
"electron:compile": "node scripts/build-electron.mjs",
|
||||
"electron:dev": "pnpm electron:compile && electron .",
|
||||
"generate:colors": "cd scripts && go run ./cmd/generate-color-system",
|
||||
"generate:emoji-sprites": "cd scripts && go run ./cmd/generate-emoji-sprites",
|
||||
"generate:masks": "npx tsx scripts/generate-avatar-masks.ts",
|
||||
"knip": "knip",
|
||||
"lingui": "pnpm lingui:extract && cd scripts && pnpm translate && pnpm lingui:compile",
|
||||
"lingui:compile": "lingui compile --strict",
|
||||
"lingui:extract": "lingui extract --clean",
|
||||
"translate": "cd scripts/cmd/locales-pending && go run . --locales-dir=../../../src/locales",
|
||||
"translate:locale": "cd scripts/cmd/locales-pending && go run . --locales-dir=../../../src/locales --locale",
|
||||
"translate:qc": "cd scripts/cmd/locales-pending && go run . --locales-dir=../../../src/locales --qc-only --qc-passes 2",
|
||||
"translate:reset": "cd scripts/cmd/locales-pending/reset && go run . --locales-dir=../../../../src/locales",
|
||||
"set:build-channel": "node scripts/set-build-channel.mjs",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:debug": "vitest run --no-coverage --inspect-brk --threads=false",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest",
|
||||
"tsc:all": "pnpm wasm:codegen && pnpm tsc -p tsconfig.json && pnpm tsc -p tsconfig.electron.json",
|
||||
"wasm:codegen": "cd crates/libfluxcore && wasm-pack build --target web --out-dir ../../pkgs/libfluxcore --release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/modifiers": "9.0.0",
|
||||
"@dnd-kit/sortable": "10.0.0",
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@electron-webauthn/native": "^0.0.6",
|
||||
"@floating-ui/react": "0.27.16",
|
||||
"@floating-ui/react-dom": "2.1.6",
|
||||
"@hcaptcha/react-hcaptcha": "1.17.2",
|
||||
"@lingui/core": "5.7.0",
|
||||
"@lingui/react": "5.7.0",
|
||||
"@livekit/components-react": "2.9.17",
|
||||
"@livekit/track-processors": "0.7.0",
|
||||
"@marsidev/react-turnstile": "1.4.0",
|
||||
"@mediapipe/tasks-vision": "0.10.14",
|
||||
"@phosphor-icons/react": "2.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@sentry/react": "10.32.1",
|
||||
"@simplewebauthn/browser": "13.2.2",
|
||||
"clsx": "2.1.1",
|
||||
"colorjs.io": "0.6.0",
|
||||
"combokeys": "3.0.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-webauthn-mac": "^1.0.0",
|
||||
"emoji-regex": "10.6.0",
|
||||
"eventemitter3": "5.0.1",
|
||||
"favico.js": "0.3.10",
|
||||
"framer-motion": "12.23.26",
|
||||
"fs-extra": "11.3.3",
|
||||
"highlight.js": "11.11.1",
|
||||
"idna-uts46-hx": "6.1.0",
|
||||
"katex": "0.16.27",
|
||||
"livekit-client": "2.16.1",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "11.2.4",
|
||||
"luxon": "3.7.2",
|
||||
"match-sorter": "8.2.0",
|
||||
"mobx": "6.15.0",
|
||||
"mobx-persist-store": "1.1.8",
|
||||
"mobx-react-lite": "4.1.1",
|
||||
"motion": "12.23.26",
|
||||
"node-mac-permissions": "^2.5.0",
|
||||
"qrcode": "1.5.4",
|
||||
"react": "19.2.3",
|
||||
"react-aria-components": "1.14.0",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-accessible-backend": "2.1.0",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dnd-multi-backend": "9.0.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hook-form": "7.69.0",
|
||||
"react-hotkeys-hook": "5.2.1",
|
||||
"react-modal-sheet": "5.2.1",
|
||||
"react-select": "5.10.2",
|
||||
"react-zoom-pan-pinch": "3.7.0",
|
||||
"rxjs": "7.8.2",
|
||||
"thumbhash": "0.1.1",
|
||||
"ua-parser-js": "2.0.7",
|
||||
"undici": "7.16.0",
|
||||
"unique-names-generator": "4.7.1",
|
||||
"update-electron-app": "3.1.2",
|
||||
"urlpattern-polyfill": "10.1.0",
|
||||
"valibot": "1.2.0",
|
||||
"ws": "8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "5.7.0",
|
||||
"@lingui/swc-plugin": "5.9.0",
|
||||
"@rspack/cli": "1.6.2",
|
||||
"@rspack/core": "1.6.2",
|
||||
"@rspack/plugin-react-refresh": "1.5.1",
|
||||
"@svgr/core": "^8.1.0",
|
||||
"@svgr/plugin-jsx": "^8.1.0",
|
||||
"@svgr/plugin-svgo": "^8.1.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/combokeys": "2.4.9",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/jsdom": "27.0.0",
|
||||
"@types/lodash": "4.17.21",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "25.0.3",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"browserslist": "^4.28.1",
|
||||
"chokidar": "^4.0.0",
|
||||
"electron": "39.2.7",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "^26.0.12",
|
||||
"esbuild": "^0.27.2",
|
||||
"happy-dom": "20.0.11",
|
||||
"jsdom": "27.4.0",
|
||||
"knip": "5.78.0",
|
||||
"lightningcss": "^1.30.2",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-discard-comments": "^7.0.5",
|
||||
"postcss-loader": "^8.2.0",
|
||||
"postcss-modules": "^6.0.1",
|
||||
"postcss-preset-env": "^10.6.0",
|
||||
"react-refresh": "^0.14.2",
|
||||
"tsx": "4.21.0",
|
||||
"typed-css-modules": "^0.9.1",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.16",
|
||||
"wasm-pack": "0.13.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron-log": "5.4.3",
|
||||
"uiohook-napi": "1.5.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome >= 47",
|
||||
"edge >= 12",
|
||||
"firefox >= 44",
|
||||
"safari >= 9",
|
||||
"ios >= 9",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"> 0.2%",
|
||||
"not op_mini all"
|
||||
],
|
||||
"packageManager": "pnpm@10.26.0",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"react": "*"
|
||||
},
|
||||
"ignoreMissing": [
|
||||
"webpack",
|
||||
"babel-plugin-macros"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@rspack/core": "1.6.2",
|
||||
"@rspack/cli": "1.6.2",
|
||||
"@lingui/swc-plugin": "5.9.0"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"core-js",
|
||||
"core-js-pure",
|
||||
"electron",
|
||||
"node-mac-permissions",
|
||||
"oxc-resolver",
|
||||
"protobufjs",
|
||||
"uiohook-napi"
|
||||
]
|
||||
}
|
||||
}
|
||||
14741
fluxer_app/pnpm-lock.yaml
generated
Normal file
43
fluxer_app/postcss.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import postcssDiscardComments from 'postcss-discard-comments';
|
||||
import postcssPresetEnv from 'postcss-preset-env';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
postcssDiscardComments({
|
||||
removeAll: true,
|
||||
}),
|
||||
postcssPresetEnv({
|
||||
stage: 3,
|
||||
features: {
|
||||
'nesting-rules': true,
|
||||
'custom-properties': true,
|
||||
'custom-media-queries': true,
|
||||
},
|
||||
browsers: 'last 10 years, > 0.5%, not dead',
|
||||
}),
|
||||
autoprefixer({
|
||||
flexbox: 'no-2009',
|
||||
grid: 'no-autoplace',
|
||||
}),
|
||||
],
|
||||
};
|
||||
33
fluxer_app/proxy/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM golang:1.25.5-alpine AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY fluxer_app/proxy/go.mod ./
|
||||
COPY fluxer_app/proxy/go.sum* ./
|
||||
RUN go mod download
|
||||
|
||||
COPY fluxer_app/proxy/ .
|
||||
|
||||
COPY fluxer_app/dist/index.html assets/index.html
|
||||
COPY fluxer_app/dist/manifest.json assets/manifest.json
|
||||
COPY fluxer_app/dist/sw.js* assets/
|
||||
COPY fluxer_app/dist/version.json assets/version.json
|
||||
|
||||
ARG TARGETOS=linux
|
||||
ARG TARGETARCH=amd64
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o fluxer-app-proxy
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /build/fluxer-app-proxy /app/fluxer-app-proxy
|
||||
|
||||
USER nobody
|
||||
|
||||
EXPOSE 8080
|
||||
ENV PORT=8080
|
||||
|
||||
ENTRYPOINT ["/app/fluxer-app-proxy"]
|
||||
2
fluxer_app/proxy/assets/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
5
fluxer_app/proxy/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/fluxerapp/fluxer/proxy
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require golang.org/x/time v0.14.0
|
||||