Skip to content

Commit

Permalink
Merge pull request #22 from shm11C3/fix_bg-image
Browse files Browse the repository at this point in the history
背景画像機能の微修正
  • Loading branch information
shm11C3 authored Nov 3, 2024
2 parents 46df43d + f494418 commit 04110f5
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 94 deletions.
18 changes: 16 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ tauri-plugin-store = "2.1.0"
base64 = "0.22"
image = "0.25.4"

[dependencies.uuid]
version = "1.11.0"
features = [
"v7", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]

[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
Expand Down
108 changes: 68 additions & 40 deletions src-tauri/src/commands/background_image.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use base64::encode;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use image::load_from_memory;
use image::ImageFormat;
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::File;
use tauri::command;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;

use crate::utils::file::get_app_data_dir;

Expand All @@ -14,17 +16,25 @@ const BG_IMG_DIR_NAME: &str = "BgImages";
///
/// 背景画像を取得
///
/// - `file_id`: 画像ファイルのインデックス
/// - `file_id`: 画像ファイルID
///
#[command]
pub fn get_background_image(file_id: String) -> Result<String, String> {
pub async fn get_background_image(file_id: String) -> Result<String, String> {
let dir_path = get_app_data_dir(BG_IMG_DIR_NAME);

// App/BgImages ディレクトリが存在しない場合新規作成
if !dir_path.exists() {
fs::create_dir_all(&dir_path)
.await
.map_err(|e| format!("Failed to create directory: {}", e))?;
}

let file_name = FILE_NAME_FORMAT.replace("{}", &file_id);
let file_path = dir_path.join(file_name);

// 画像を読み込んでBase64にエンコード
match fs::read(&file_path) {
Ok(image_data) => Ok(encode(image_data)),
match fs::read(&file_path).await {
Ok(image_data) => Ok(STANDARD.encode(image_data)),
Err(e) => Err(format!("Failed to load image: {}", e)),
}
}
Expand All @@ -44,27 +54,39 @@ pub struct BackgroundImage {
/// BG_IMG_DIR_NAME ディレクトリ内の背景画像一覧を取得
///
#[command]
pub fn get_background_images() -> Result<Vec<BackgroundImage>, String> {
pub async fn get_background_images() -> Result<Vec<BackgroundImage>, String> {
let dir_path = get_app_data_dir(BG_IMG_DIR_NAME);

// App/BgImages ディレクトリが存在しない場合新規作成
if !dir_path.exists() {
fs::create_dir_all(&dir_path)
.await
.map_err(|e| format!("Failed to create directory: {}", e))?;
}

// ディレクトリ内のファイル一覧を取得
match fs::read_dir(&dir_path) {
Ok(entries) => {
let images: Vec<BackgroundImage> = entries
.filter_map(|entry| {
let entry = entry.ok()?;
let file_name = entry.file_name().into_string().ok()?;
let file_id = file_name
match fs::read_dir(&dir_path).await {
Ok(mut entries) => {
let mut images: Vec<BackgroundImage> = Vec::new();

while let Some(entry) = entries.next_entry().await.ok().flatten() {
if let Some(file_name) = entry.file_name().to_str() {
if let Some(file_id) = file_name
.strip_prefix("bg-img-")
.and_then(|s| s.strip_suffix(".png"))?;
let file_path = entry.path();
let image_data = fs::read(&file_path).ok().map(|data| encode(data))?;
Some(BackgroundImage {
file_id: file_id.to_string(),
image_data,
})
})
.collect();
.and_then(|s| s.strip_suffix(".png"))
{
let file_path = entry.path();
if let Ok(image_data) = fs::read(&file_path).await {
images.push(BackgroundImage {
file_id: file_id.to_string(),
image_data: STANDARD.encode(image_data),
});
}
}
}
}

images.sort_by(|a, b| b.file_id.cmp(&a.file_id));
Ok(images)
}
Err(e) => Err(format!("Failed to read directory: {}", e)),
Expand All @@ -78,12 +100,14 @@ pub fn get_background_images() -> Result<Vec<BackgroundImage>, String> {
/// - returns: `file_id`
///
#[command]
pub fn save_background_image(image_data: String) -> Result<String, String> {
pub async fn save_background_image(image_data: String) -> Result<String, String> {
let dir_path = get_app_data_dir(BG_IMG_DIR_NAME);

// App/BgImages ディレクトリが存在しない場合新規作成
if !dir_path.parent().unwrap().exists() {
fs::create_dir_all(dir_path.parent().unwrap()).unwrap();
if !dir_path.exists() {
fs::create_dir_all(&dir_path)
.await
.map_err(|e| format!("Failed to create directory: {}", e))?;
}

// Base64データのプレフィックスを除去
Expand All @@ -96,26 +120,30 @@ pub fn save_background_image(image_data: String) -> Result<String, String> {
// 改行や余分な空白を除去
let cleaned_data = image_data.replace("\n", "").replace("\r", "");

// ディレクトリ内のファイル数を取得し、それをインデックスとして利用
let file_id: String = match fs::read_dir(&dir_path) {
Ok(entries) => entries.count(), // 現在のファイル数をインデックスとして利用
Err(_) => 0, // 読み込み失敗の場合は最初のファイルとして 0
}
.to_string();

let file_id = Uuid::now_v7().to_string();
let file_name = FILE_NAME_FORMAT.replace("{}", &file_id);
let file_path = dir_path.join(file_name);

// Base64データをデコード
match base64::decode(&cleaned_data) {
match STANDARD.decode(&cleaned_data) {
Ok(decoded_data) => {
// 画像データをPNGとして保存
match load_from_memory(&decoded_data) {
Ok(image) => {
let mut file = File::create(&file_path)
let mut file = fs::File::create(&file_path)
.await
.map_err(|e| format!("Failed to create file: {}", e))?;

// 非同期で画像データを書き込む
let mut buffer = Vec::new();
let mut cursor = std::io::Cursor::new(&mut buffer);
image
.write_to(&mut file, ImageFormat::Png)
.write_to(&mut cursor, ImageFormat::Png)
.map_err(|e| format!("Failed to convert image to PNG format: {}", e))?;

file
.write_all(&buffer)
.await
.map_err(|e| format!("Failed to save image as PNG: {}", e))?;
Ok(file_id)
}
Expand All @@ -128,15 +156,15 @@ pub fn save_background_image(image_data: String) -> Result<String, String> {

///
/// 背景画像を削除
/// - `file_id`: 画像ファイルのインデックス
/// - `file_id`: 画像ファイルID
///
#[tauri::command]
pub fn delete_background_image(file_id: String) -> Result<(), String> {
pub async fn delete_background_image(file_id: String) -> Result<(), String> {
let dir_path = get_app_data_dir(BG_IMG_DIR_NAME);
let file_name = FILE_NAME_FORMAT.replace("{}", &file_id);
let file_path = dir_path.join(file_name);

match fs::remove_file(&file_path) {
match fs::remove_file(&file_path).await {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to delete image: {}", e)),
}
Expand Down
41 changes: 27 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import Dashboard from "./template/Dashboard";
import ChartTemplate from "./template/Usage";
import "./index.css";
Expand Down Expand Up @@ -26,7 +26,10 @@ const onError = (error: Error, info: ErrorInfo) => {
const Page = () => {
const { settings } = useSettingsAtom();
const { toggle } = useDarkMode();
const { backgroundImage } = useBackgroundImage();
const { backgroundImage: nextImage } = useBackgroundImage();

const [currentImage, setCurrentImage] = useState(nextImage);
const [opacity, setOpacity] = useState(1);

useErrorModalListener();
useUsageUpdater("cpu");
Expand All @@ -41,6 +44,16 @@ const Page = () => {
}
}, [settings.theme, toggle]);

useEffect(() => {
setOpacity(0);
const fadeOutTimeout = setTimeout(() => {
setCurrentImage(nextImage);
setOpacity(1);
}, 500);

return () => clearTimeout(fadeOutTimeout);
}, [nextImage]);

const displayTargets: Record<SelectedDisplayType, JSX.Element> = {
dashboard: (
<ScreenTemplate>
Expand All @@ -57,18 +70,18 @@ const Page = () => {

return (
<ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
<div className="bg-zinc-200 dark:bg-gray-900 text-gray-900 dark:text-white min-h-screen bg-cover">
{backgroundImage && (
<div
className="fixed inset-0 bg-cover bg-center"
style={{
backgroundImage: `url(${backgroundImage})`,
backgroundAttachment: "fixed",
backgroundSize: "cover",
opacity: settings.backgroundImgOpacity / 100,
}}
/>
)}
<div className="bg-zinc-200 dark:bg-gray-900 text-gray-900 dark:text-white min-h-screen bg-cover ease-in-out">
<div
className="fixed inset-0 bg-cover bg-center transition-opacity duration-500"
style={{
backgroundImage: `url(${currentImage})`,
backgroundAttachment: "fixed",
backgroundSize: "cover",
opacity: currentImage
? opacity * (settings.backgroundImgOpacity / 100)
: 0,
}}
/>
<div className="relative z-10">
<SideMenu />
{displayTargets[settings.state.display]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export const BackgroundImageList = () => {
});

return (
<div className="flex py-3">
<div className="flex py-3 max-w-full overflow-x-auto">
{backgroundImageList.length > 0 && (
<Button
className={twMerge(
selectImageVariants({
selected: !settings.selectedBackgroundImg,
}),
"flex items-center justify-center bg-zinc-300 dark:bg-gray-800 hover:bg-zinc-200 dark:hover:bg-gray-900",
"flex items-center justify-center min-w-20 bg-zinc-300 dark:bg-gray-800 hover:bg-zinc-200 dark:hover:bg-gray-900",
)}
onClick={() => {
updateSettingAtom("selectedBackgroundImg", null);
Expand All @@ -38,38 +38,35 @@ export const BackgroundImageList = () => {
</Button>
)}

{backgroundImageList
.slice()
.reverse()
.map((image) => (
<div key={image.fileId} className="relative">
<button
className="absolute top-[-6px] right-[-4px] p-1 text-white bg-gray-500 bg-opacity-80 rounded-full z-20"
type="button"
onClick={() => deleteBackgroundImage(image.fileId)}
>
<X />
</button>
<button
type="button"
className={twMerge(
selectImageVariants({
selected: settings.selectedBackgroundImg === image.fileId,
}),
"overflow-hidden",
)}
onClick={() =>
updateSettingAtom("selectedBackgroundImg", image.fileId)
}
>
<img
src={image.imageData}
alt={`background image: ${image.fileId}`}
className="object-cover w-full h-full opacity-50"
/>
</button>
</div>
))}
{backgroundImageList.map((image) => (
<div key={image.fileId} className="relative">
<button
className="absolute top-[-6px] right-[-4px] p-1 text-white bg-gray-500 bg-opacity-80 rounded-full z-20"
type="button"
onClick={() => deleteBackgroundImage(image.fileId)}
>
<X />
</button>
<button
type="button"
className={twMerge(
selectImageVariants({
selected: settings.selectedBackgroundImg === image.fileId,
}),
"overflow-hidden",
)}
onClick={() =>
updateSettingAtom("selectedBackgroundImg", image.fileId)
}
>
<img
src={image.imageData}
alt={`background image: ${image.fileId}`}
className="object-cover w-full h-full opacity-50"
/>
</button>
</div>
))}
</div>
);
};
Loading

0 comments on commit 04110f5

Please sign in to comment.