Compare commits
2 commits
37f0c72114
...
315746852b
Author | SHA1 | Date | |
---|---|---|---|
|
315746852b | ||
|
ca9f143b30 |
|
@ -22,6 +22,7 @@ rocket_cors = "0.6"
|
|||
lazy_static = "1.4"
|
||||
password-hash = "0.5"
|
||||
argon2 = "0.5"
|
||||
utoipa = "5.3.1"
|
||||
|
||||
[build-dependencies]
|
||||
dotenv = "0.15"
|
||||
|
|
137
api/src/file.rs
|
@ -15,8 +15,9 @@ use crate::encryption::{encrypt_data, decrypt_data};
|
|||
use crate::models::{File, Share};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct Upload<'r> {
|
||||
file: TempFile<'r>,
|
||||
pub struct Upload<'f> {
|
||||
pub file: TempFile<'f>,
|
||||
pub filename: Option<String>,
|
||||
}
|
||||
|
||||
#[post("/upload", data = "<upload>", format = "multipart/form-data")]
|
||||
|
@ -26,32 +27,28 @@ pub async fn upload_file(
|
|||
upload: Form<Upload<'_>>
|
||||
) -> Result<Status, Status> {
|
||||
let mut buffer = Vec::new();
|
||||
if let Err(_) = {
|
||||
let mut file = match upload.file.open().await {
|
||||
Ok(file) => file,
|
||||
Err(_) => return Err(Status::BadRequest),
|
||||
};
|
||||
file.read_to_end(&mut buffer).await
|
||||
} {
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
let mut file = upload.file.open().await.map_err(|_| Status::BadRequest)?;
|
||||
file.read_to_end(&mut buffer).await.map_err(|_| Status::BadRequest)?;
|
||||
|
||||
let nonce = rand::random::<[u8; 12]>();
|
||||
let key = std::env::var("ENCRYPTION_KEY").expect("ENCRYPTION_KEY must be set");
|
||||
let encrypted = encrypt_data(&buffer, key.as_bytes(), &nonce);
|
||||
|
||||
let file_id = Uuid::new_v4().to_string();
|
||||
let storage_path = format!("./data/{}", file_id);
|
||||
let user_id = user.user_id.ok_or(Status::BadRequest)?;
|
||||
let user_dir = format!("./data/{}", user_id);
|
||||
let storage_path = format!("{}/{}", &user_dir, file_id);
|
||||
|
||||
fs::write(&storage_path, &encrypted).await.ok();
|
||||
fs::write(format!("{}.nonce", &storage_path), &nonce).await.ok();
|
||||
fs::create_dir_all(&user_dir).await.map_err(|_| Status::InternalServerError)?;
|
||||
fs::write(&storage_path, &encrypted).await.map_err(|_| Status::InternalServerError)?;
|
||||
fs::write(format!("{}.nonce", &storage_path), &nonce).await.map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
let original_name = upload.file.name().unwrap_or("file.bin").to_string();
|
||||
let original_name = upload.filename.as_deref().ok_or(Status::BadRequest)?;
|
||||
let size = buffer.len() as i64;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO files (user_id, original_name, storage_path, uploaded_at, size) VALUES ($1, $2, $3, $4, $5)",
|
||||
user.user_id,
|
||||
user_id,
|
||||
original_name,
|
||||
storage_path,
|
||||
Utc::now(),
|
||||
|
@ -64,6 +61,7 @@ pub async fn upload_file(
|
|||
Ok(Status::Created)
|
||||
}
|
||||
|
||||
|
||||
#[get("/files")]
|
||||
pub async fn list_user_files(pool: &State<PgPool>, user: AuthenticatedUser) -> Json<Vec<File>> {
|
||||
let Some(user_id) = user.user_id else {
|
||||
|
@ -184,15 +182,7 @@ pub async fn share_file(pool: &State<PgPool>, user: AuthenticatedUser, req: Json
|
|||
.await
|
||||
.map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
let share = sqlx::query_as::<_, Share>(
|
||||
"SELECT id, file_id, shared_by, created_at, expires_at FROM shares WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
Ok(Json(share.id.to_string()))
|
||||
Ok(Json(id.to_string()))
|
||||
}
|
||||
|
||||
#[get("/shared/<link>")]
|
||||
|
@ -241,3 +231,100 @@ pub async fn download_shared(pool: &State<PgPool>, link: &str) -> Option<FileDow
|
|||
filename: file.original_name,
|
||||
})
|
||||
}
|
||||
|
||||
#[delete("/shares/<share_id>")]
|
||||
pub async fn delete_share(
|
||||
pool: &State<PgPool>,
|
||||
user: AuthenticatedUser,
|
||||
share_id: &str
|
||||
) -> Status {
|
||||
let Some(user_id) = user.user_id else {
|
||||
return Status::Unauthorized;
|
||||
};
|
||||
|
||||
// Parse the UUID
|
||||
let uuid = match Uuid::parse_str(share_id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Status::BadRequest,
|
||||
};
|
||||
|
||||
// Check if the share exists and belongs to the user
|
||||
let share = sqlx::query!(
|
||||
"SELECT id FROM shares WHERE id = $1 AND shared_by = $2",
|
||||
uuid,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(pool.inner())
|
||||
.await;
|
||||
|
||||
match share {
|
||||
Ok(Some(_)) => {
|
||||
// Delete the share
|
||||
match sqlx::query!("DELETE FROM shares WHERE id = $1", uuid)
|
||||
.execute(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => Status::NoContent,
|
||||
Err(_) => Status::InternalServerError,
|
||||
}
|
||||
}
|
||||
Ok(None) => Status::NotFound,
|
||||
Err(_) => Status::InternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ShareInfo {
|
||||
pub id: String,
|
||||
pub file_id: i32,
|
||||
pub file_name: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub expires_at: Option<chrono::NaiveDateTime>,
|
||||
pub is_expired: bool,
|
||||
}
|
||||
|
||||
#[get("/shares")]
|
||||
pub async fn list_user_shares(pool: &State<PgPool>, user: AuthenticatedUser) -> Json<Vec<ShareInfo>> {
|
||||
let Some(user_id) = user.user_id else {
|
||||
return Json(Vec::new());
|
||||
};
|
||||
|
||||
let shares = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
s.id,
|
||||
s.file_id,
|
||||
s.created_at,
|
||||
s.expires_at,
|
||||
f.original_name as file_name
|
||||
FROM shares s
|
||||
JOIN files f ON s.file_id = f.id
|
||||
WHERE s.shared_by = $1
|
||||
ORDER BY s.created_at DESC
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let share_infos = shares
|
||||
.into_iter()
|
||||
.map(|record| {
|
||||
let expires_at = record.expires_at.map(|dt| dt.naive_utc());
|
||||
let is_expired = expires_at.map_or(false, |exp| exp < now);
|
||||
|
||||
ShareInfo {
|
||||
id: record.id.to_string(),
|
||||
file_id: record.file_id.unwrap_or(0),
|
||||
file_name: record.file_name,
|
||||
created_at: record.created_at,
|
||||
expires_at,
|
||||
is_expired,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(share_infos)
|
||||
}
|
|
@ -7,12 +7,13 @@ use rocket::http::Status;
|
|||
use rocket::response::status::Custom;
|
||||
use rocket::data::ByteUnit;
|
||||
|
||||
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
use std::env;
|
||||
use dotenv::dotenv;
|
||||
|
||||
use rocket_cors::{CorsOptions, AllowedOrigins};
|
||||
use rocket_cors::{CorsOptions};
|
||||
|
||||
mod auth;
|
||||
mod encryption;
|
||||
|
@ -67,7 +68,9 @@ async fn main() -> Result<(), rocket::Error> {
|
|||
download_file,
|
||||
download_shared,
|
||||
list_user_files,
|
||||
list_user_shares,
|
||||
delete_file,
|
||||
delete_share,
|
||||
share_file,
|
||||
update_role,
|
||||
update_quota,
|
||||
|
|
|
@ -25,7 +25,7 @@ pub struct File {
|
|||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Share {
|
||||
pub id: Uuid,
|
||||
pub file_id: Option<i32>, // Changed to Option<i32>
|
||||
pub file_id: Option<i32>,
|
||||
pub shared_by: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub expires_at: Option<chrono::NaiveDateTime>,
|
||||
|
|
24
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
54
frontend/README.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
28
frontend/eslint.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
13
frontend/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
4087
frontend/package-lock.json
generated
Normal file
34
frontend/package.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"lucide-react": "^0.513.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"postcss": "^8.5.4",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
6
frontend/postcss.config.cjs
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
1
frontend/public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
42
frontend/src/App.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
82
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Login } from "./components/Login";
|
||||
import { Register } from "./components/Register";
|
||||
import { MainPage } from "./components/MainPage";
|
||||
import { ThemeToggle } from "./components/ThemeToggle";
|
||||
|
||||
const TOKEN_STORAGE_KEY = "litecloud_token";
|
||||
const THEME_STORAGE_KEY = "litecloud_theme";
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showRegister, setShowRegister] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const savedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
if (savedToken) {
|
||||
setToken(savedToken);
|
||||
}
|
||||
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (savedTheme !== null) {
|
||||
setIsDarkMode(savedTheme === 'dark');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleLoginSuccess = (token: string) => {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
setToken(token);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
setToken(null);
|
||||
setShowRegister(false);
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = !isDarkMode;
|
||||
setIsDarkMode(newTheme);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<>
|
||||
<ThemeToggle isDarkMode={isDarkMode} onToggle={toggleTheme} />
|
||||
{showRegister ? (
|
||||
<Register
|
||||
onSwitchToLogin={() => setShowRegister(false)}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
onSwitchToRegister={() => setShowRegister(true)}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeToggle isDarkMode={isDarkMode} onToggle={toggleTheme} />
|
||||
<MainPage isDarkMode={isDarkMode} onLogout={handleLogout} />
|
||||
</>
|
||||
);
|
||||
};
|
1
frontend/src/assets/react.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4 KiB |
275
frontend/src/components/FileComponent.tsx
Normal file
|
@ -0,0 +1,275 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { ChevronDown, Upload, Share, Trash2 } from "lucide-react";
|
||||
|
||||
interface File {
|
||||
id: number;
|
||||
user_id: number;
|
||||
original_name: string;
|
||||
storage_path: string;
|
||||
uploaded_at: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface FilesComponentProps {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) => {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dropdownRef = useRef<HTMLTableDataCellElement | null>(null);
|
||||
|
||||
// Recupera il token solo una volta al mount o quando cambia
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
setToken(storedToken);
|
||||
}, []);
|
||||
|
||||
const apiCall = async (url: string, options: RequestInit = {}) => {
|
||||
if (!token) throw new Error('No auth token found');
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const fetchFiles = async () => {
|
||||
if (!token) return; // Non chiamare se il token non è pronto
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await apiCall('http://192.168.1.120:8082/api/files');
|
||||
const filesData = await response.json();
|
||||
setFiles(filesData);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch files');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (token) fetchFiles();
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setOpenDropdown(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('filename', file.name);
|
||||
|
||||
const token = getAuthToken();
|
||||
const response = await fetch('http://192.168.1.120:8082/api/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSuccess('File uploaded successfully!');
|
||||
fetchFiles();
|
||||
} else {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to upload file');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileAction = async (action: 'share' | 'delete', fileId: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (action === 'share') {
|
||||
const response = await apiCall('http://192.168.1.120:8082/api/share', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
expires_in_days: 7
|
||||
}),
|
||||
});
|
||||
const shareId = await response.json();
|
||||
setSuccess(`Share link created: /shared/${shareId}`);
|
||||
|
||||
} else if (action === 'delete') {
|
||||
await apiCall(`http://192.168.1.120:8082/api/files/${fileId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
setSuccess('File deleted successfully');
|
||||
fetchFiles();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Failed to ${action} file`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setOpenDropdown(null);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="component-container">
|
||||
{/* Success/Error Messages */}
|
||||
{error && (
|
||||
<div className="notification notification-error">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="notification-close">×</button>
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="notification notification-success">
|
||||
{success}
|
||||
<button onClick={() => setSuccess(null)} className="notification-close">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="component-header">
|
||||
<h1 className={`component-title ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
|
||||
My Files
|
||||
</h1>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={loading}
|
||||
className={`upload-button ${
|
||||
isDarkMode
|
||||
? 'upload-button-dark'
|
||||
: 'upload-button-light'
|
||||
}`}
|
||||
>
|
||||
<Upload size={18} />
|
||||
Upload File
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="loading-center">
|
||||
<div className="loading-spinner-small"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`table-container ${isDarkMode ? 'table-container-dark' : 'table-container-light'}`}>
|
||||
<table className="data-table">
|
||||
<thead className={isDarkMode ? 'table-header-dark' : 'table-header-light'}>
|
||||
<tr>
|
||||
<th className="table-header-cell">Name</th>
|
||||
<th className="table-header-cell">Size</th>
|
||||
<th className="table-header-cell">Uploaded</th>
|
||||
<th className="table-header-cell">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={`table-body ${isDarkMode ? 'table-body-dark' : 'table-body-light'}`}>
|
||||
{files.map((file) => (
|
||||
<tr key={file.id} className={isDarkMode ? 'table-row-dark' : 'table-row-light'}>
|
||||
<td className={`table-cell ${isDarkMode ? 'text-white' : 'text-gray-900'}`}>
|
||||
{file.original_name}
|
||||
</td>
|
||||
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>
|
||||
{formatFileSize(file.size)}
|
||||
</td>
|
||||
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>
|
||||
{formatDate(file.uploaded_at)}
|
||||
</td>
|
||||
<td className="table-cell relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setOpenDropdown(openDropdown === file.id ? null : file.id)}
|
||||
className={`actions-button ${
|
||||
isDarkMode ? 'actions-button-dark' : 'actions-button-light'
|
||||
}`}
|
||||
>
|
||||
Actions
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
|
||||
{openDropdown === file.id && (
|
||||
<div className={`dropdown-menu ${
|
||||
isDarkMode ? 'dropdown-menu-dark' : 'dropdown-menu-light'
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => handleFileAction('share', file.id)}
|
||||
className={`dropdown-item ${
|
||||
isDarkMode ? 'dropdown-item-dark' : 'dropdown-item-light'
|
||||
}`}
|
||||
>
|
||||
<Share size={16} />
|
||||
Share
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFileAction('delete', file.id)}
|
||||
className="dropdown-item dropdown-item-danger"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{files.length === 0 && !loading && (
|
||||
<div className={`empty-state ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
No files uploaded yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
108
frontend/src/components/Login.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
interface LoginProps {
|
||||
onLoginSuccess: (token: string) => void;
|
||||
onSwitchToRegister: () => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const Login: React.FC<LoginProps> = ({
|
||||
onLoginSuccess,
|
||||
onSwitchToRegister,
|
||||
isDarkMode
|
||||
}) => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch("http://192.168.1.120:8082/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
throw new Error(`Login failed: ${res.status} ${res.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
onLoginSuccess(data.token);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`gradient-bg ${isDarkMode ? 'gradient-bg-dark' : 'gradient-bg-light'}`}>
|
||||
<div
|
||||
className={`auth-container ${
|
||||
isDarkMode ? 'auth-container-dark' : 'auth-container-light'
|
||||
}`}
|
||||
>
|
||||
<h2 className={`auth-title ${isDarkMode ? 'text-white' : 'text-gray-500'}`}>
|
||||
Login - Litecloud
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className={`auth-input ${
|
||||
isDarkMode ? 'auth-input-dark' : 'auth-input-light'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={`auth-input ${
|
||||
isDarkMode ? 'auth-input-dark' : 'auth-input-light'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`auth-button ${
|
||||
isDarkMode ? 'auth-button-dark' : 'auth-button-light'
|
||||
}`}
|
||||
>
|
||||
{loading ? "Logging in..." : "Login"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSwitchToRegister}
|
||||
className={`auth-switch ${
|
||||
isDarkMode ? 'auth-switch-dark' : 'auth-switch-light'
|
||||
}`}
|
||||
>
|
||||
New user? Register now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
119
frontend/src/components/MainPage.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import React, { useState } from "react";
|
||||
import { Files, Share, Settings, LogOut } from "lucide-react";
|
||||
import { FilesComponent } from "./FileComponent";
|
||||
import { SettingsComponent } from "./Settings";
|
||||
import { SharesComponent } from "./ShareComponent";
|
||||
|
||||
interface MainPageProps {
|
||||
isDarkMode: boolean;
|
||||
onLogout: () => void;
|
||||
onThemeToggle: () => void;
|
||||
}
|
||||
|
||||
interface GradientColors {
|
||||
dark: { from: string; to: string };
|
||||
light: { from: string; to: string };
|
||||
}
|
||||
|
||||
export const MainPage: React.FC<MainPageProps> = ({
|
||||
isDarkMode,
|
||||
onLogout,
|
||||
onThemeToggle
|
||||
}) => {
|
||||
const [activeView, setActiveView] = useState<'files' | 'shares' | 'settings'>('files');
|
||||
const [gradientColors, setGradientColors] = useState<GradientColors>({
|
||||
dark: { from: '#7b2ff7', to: '#f107a3' },
|
||||
light: { from: '#06b6d4', to: '#3b82f6' }
|
||||
});
|
||||
|
||||
const currentGradient = isDarkMode ? gradientColors.dark : gradientColors.light;
|
||||
const gradientStyle = {
|
||||
background: `linear-gradient(135deg, ${currentGradient.from} 0%, ${currentGradient.to} 100%)`
|
||||
};
|
||||
|
||||
const renderActiveView = () => {
|
||||
switch (activeView) {
|
||||
case 'files':
|
||||
return <FilesComponent isDarkMode={isDarkMode} />;
|
||||
case 'shares':
|
||||
return <SharesComponent isDarkMode={isDarkMode} />;
|
||||
case 'settings':
|
||||
return (
|
||||
<SettingsComponent
|
||||
isDarkMode={isDarkMode}
|
||||
onThemeToggle={onThemeToggle}
|
||||
gradientColors={gradientColors}
|
||||
setGradientColors={setGradientColors}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <FilesComponent isDarkMode={isDarkMode} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-container" style={gradientStyle}>
|
||||
<div className="app-layout">
|
||||
{/* Sidebar */}
|
||||
<div className={`sidebar ${isDarkMode ? 'sidebar-dark' : 'sidebar-light'}`}>
|
||||
<h2 className={`sidebar-title ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
|
||||
LiteCloud
|
||||
</h2>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
<button
|
||||
onClick={() => setActiveView('files')}
|
||||
className={`nav-button ${
|
||||
activeView === 'files'
|
||||
? isDarkMode ? 'nav-button-active-dark' : 'nav-button-active-light'
|
||||
: isDarkMode ? 'nav-button-inactive-dark' : 'nav-button-inactive-light'
|
||||
}`}
|
||||
>
|
||||
<Files size={20} />
|
||||
My Files
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView('shares')}
|
||||
className={`nav-button ${
|
||||
activeView === 'shares'
|
||||
? isDarkMode ? 'nav-button-active-dark' : 'nav-button-active-light'
|
||||
: isDarkMode ? 'nav-button-inactive-dark' : 'nav-button-inactive-light'
|
||||
}`}
|
||||
>
|
||||
<Share size={20} />
|
||||
Shares
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView('settings')}
|
||||
className={`nav-button ${
|
||||
activeView === 'settings'
|
||||
? isDarkMode ? 'nav-button-active-dark' : 'nav-button-active-light'
|
||||
: isDarkMode ? 'nav-button-inactive-dark' : 'nav-button-inactive-light'
|
||||
}`}
|
||||
>
|
||||
<Settings size={20} />
|
||||
Settings
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="logout-button-sidebar"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className={`main-content ${isDarkMode ? 'main-content-dark' : 'main-content-light'}`}>
|
||||
{renderActiveView()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
116
frontend/src/components/Register.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
interface RegisterProps {
|
||||
onSwitchToLogin: () => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const Register: React.FC<RegisterProps> = ({
|
||||
onSwitchToLogin,
|
||||
isDarkMode
|
||||
}) => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const res = await fetch("http://localhost:8000/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
throw new Error(`Registration failed: ${res.status} ${res.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
onSwitchToLogin();
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`gradient-bg ${isDarkMode ? 'gradient-bg-dark' : 'gradient-bg-light'}`}>
|
||||
<div
|
||||
className={`auth-container ${
|
||||
isDarkMode ? 'auth-container-dark' : 'auth-container-light'
|
||||
}`}
|
||||
>
|
||||
<h2 className={`auth-title ${isDarkMode ? 'text-white' : 'text-gray-900'}`}>
|
||||
Register
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="success-message">
|
||||
Registration successful! Redirecting to login...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className={`auth-input ${
|
||||
isDarkMode ? 'auth-input-dark' : 'auth-input-light'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={`auth-input ${
|
||||
isDarkMode ? 'auth-input-dark' : 'auth-input-light'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`auth-button ${
|
||||
isDarkMode ? 'auth-button-dark' : 'auth-button-light'
|
||||
}`}
|
||||
>
|
||||
{loading ? "Registering..." : "Register"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSwitchToLogin}
|
||||
className={`auth-switch ${
|
||||
isDarkMode ? 'auth-switch-dark' : 'auth-switch-light'
|
||||
}`}
|
||||
>
|
||||
Already have an account? Login now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
164
frontend/src/components/Settings.tsx
Normal file
|
@ -0,0 +1,164 @@
|
|||
import React from "react";
|
||||
import { Sun, Moon, Palette } from "lucide-react";
|
||||
|
||||
interface GradientColors {
|
||||
dark: { from: string; to: string };
|
||||
light: { from: string; to: string };
|
||||
}
|
||||
|
||||
interface SettingsComponentProps {
|
||||
isDarkMode: boolean;
|
||||
onThemeToggle: () => void;
|
||||
gradientColors: GradientColors;
|
||||
setGradientColors: React.Dispatch<React.SetStateAction<GradientColors>>;
|
||||
}
|
||||
|
||||
export const SettingsComponent: React.FC<SettingsComponentProps> = ({
|
||||
isDarkMode,
|
||||
onThemeToggle,
|
||||
gradientColors,
|
||||
setGradientColors
|
||||
}) => {
|
||||
const ColorPicker: React.FC<{
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void
|
||||
}> = ({ label, value, onChange }) => (
|
||||
<div className="color-picker-container">
|
||||
<span className={`color-picker-label ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
{label}
|
||||
</span>
|
||||
<input
|
||||
type="color"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="color-picker-input"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="component-container">
|
||||
<div className="component-header">
|
||||
<h1 className={`component-title ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
|
||||
Settings
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="settings-content">
|
||||
{/* Theme Toggle */}
|
||||
<div className="setting-item">
|
||||
<span className={`setting-label ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
|
||||
Dark Mode
|
||||
</span>
|
||||
<button
|
||||
onClick={onThemeToggle}
|
||||
className={`theme-toggle-button ${
|
||||
isDarkMode
|
||||
? 'theme-toggle-button-dark'
|
||||
: 'theme-toggle-button-light'
|
||||
}`}
|
||||
>
|
||||
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
|
||||
{isDarkMode ? 'Light' : 'Dark'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Gradient Colors */}
|
||||
<div className={`gradient-settings ${
|
||||
isDarkMode ? 'gradient-settings-dark' : 'gradient-settings-light'
|
||||
}`}>
|
||||
<h3 className={`gradient-settings-title ${
|
||||
isDarkMode ? 'text-white' : 'text-gray-800'
|
||||
}`}>
|
||||
<Palette size={18} />
|
||||
Gradient Colors
|
||||
</h3>
|
||||
|
||||
<div className="gradient-settings-content">
|
||||
<div className="gradient-group">
|
||||
<h4 className={`gradient-group-title ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-600'
|
||||
}`}>
|
||||
Dark Mode Gradient
|
||||
</h4>
|
||||
<div className="gradient-controls">
|
||||
<ColorPicker
|
||||
label="From"
|
||||
value={gradientColors.dark.from}
|
||||
onChange={(value) => setGradientColors(prev => ({
|
||||
...prev,
|
||||
dark: { ...prev.dark, from: value }
|
||||
}))}
|
||||
/>
|
||||
<ColorPicker
|
||||
label="To"
|
||||
value={gradientColors.dark.to}
|
||||
onChange={(value) => setGradientColors(prev => ({
|
||||
...prev,
|
||||
dark: { ...prev.dark, to: value }
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gradient-preview"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${gradientColors.dark.from} 0%, ${gradientColors.dark.to} 100%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gradient-group">
|
||||
<h4 className={`gradient-group-title ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-600'
|
||||
}`}>
|
||||
Light Mode Gradient
|
||||
</h4>
|
||||
<div className="gradient-controls">
|
||||
<ColorPicker
|
||||
label="From"
|
||||
value={gradientColors.light.from}
|
||||
onChange={(value) => setGradientColors(prev => ({
|
||||
...prev,
|
||||
light: { ...prev.light, from: value }
|
||||
}))}
|
||||
/>
|
||||
<ColorPicker
|
||||
label="To"
|
||||
value={gradientColors.light.to}
|
||||
onChange={(value) => setGradientColors(prev => ({
|
||||
...prev,
|
||||
light: { ...prev.light, to: value }
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gradient-preview"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${gradientColors.light.from} 0%, ${gradientColors.light.to} 100%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Settings Placeholder */}
|
||||
<div className={`settings-section ${
|
||||
isDarkMode ? 'settings-section-dark' : 'settings-section-light'
|
||||
}`}>
|
||||
<h3 className={`settings-section-title ${
|
||||
isDarkMode ? 'text-white' : 'text-gray-800'
|
||||
}`}>
|
||||
Coming Soon
|
||||
</h3>
|
||||
<p className={`settings-section-description ${
|
||||
isDarkMode ? 'text-gray-400' : 'text-gray-500'
|
||||
}`}>
|
||||
More settings will be available in future updates, including file management preferences,
|
||||
notification settings, and account management options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
168
frontend/src/components/ShareComponent.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
interface ShareInfo {
|
||||
id: string;
|
||||
file_id: number;
|
||||
file_name: string;
|
||||
created_at: string;
|
||||
expires_at: string | null;
|
||||
is_expired: boolean;
|
||||
}
|
||||
|
||||
interface SharesComponentProps {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const SharesComponent: React.FC<SharesComponentProps> = ({ isDarkMode }) => {
|
||||
const [shares, setShares] = useState<ShareInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const getAuthToken = () => {
|
||||
return localStorage.getItem('authToken') || '';
|
||||
};
|
||||
|
||||
const apiCall = async (url: string, options: RequestInit = {}) => {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const fetchShares = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await apiCall('/api/shares');
|
||||
const sharesData = await response.json();
|
||||
setShares(sharesData);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch shares');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchShares();
|
||||
}, []);
|
||||
|
||||
const handleDeleteShare = async (shareId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
await apiCall(`/api/shares/${shareId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
setSuccess('Share deleted successfully');
|
||||
fetchShares();
|
||||
} catch (err) {
|
||||
setError('Failed to delete share');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const copyToClipboard = async (shareId: string) => {
|
||||
try {
|
||||
const shareUrl = `${window.location.origin}/shared/${shareId}`;
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
setSuccess('Share link copied to clipboard!');
|
||||
} catch (err) {
|
||||
setError('Failed to copy link');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="component-container">
|
||||
{/* Success/Error Messages */}
|
||||
{error && (
|
||||
<div className="notification notification-error">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="notification-close">×</button>
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="notification notification-success">
|
||||
{success}
|
||||
<button onClick={() => setSuccess(null)} className="notification-close">×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="component-header">
|
||||
<h1 className={`component-title ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
|
||||
Shared Files
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="loading-center">
|
||||
<div className="loading-spinner-small"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="shares-grid">
|
||||
{shares.map((share) => (
|
||||
<div key={share.id} className={`share-card ${
|
||||
isDarkMode ? 'share-card-dark' : 'share-card-light'
|
||||
}`}>
|
||||
<div className="share-card-content">
|
||||
<div className="share-info">
|
||||
<h3 className={`share-title ${isDarkMode ? 'text-white' : 'text-gray-900'}`}>
|
||||
{share.file_name}
|
||||
</h3>
|
||||
<p className={`share-detail ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Created: {formatDate(share.created_at)}
|
||||
</p>
|
||||
{share.expires_at && (
|
||||
<p className={`share-detail ${
|
||||
share.is_expired ? 'text-red-500' : isDarkMode ? 'text-gray-400' : 'text-gray-500'
|
||||
}`}>
|
||||
{share.is_expired ? 'Expired' : 'Expires'}: {formatDate(share.expires_at)}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => copyToClipboard(share.id)}
|
||||
className={`share-link ${isDarkMode ? 'share-link-dark' : 'share-link-light'}`}
|
||||
title="Click to copy link"
|
||||
>
|
||||
/shared/{share.id.substring(0, 8)}...
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteShare(share.id)}
|
||||
className="delete-share-button"
|
||||
title="Delete share"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{shares.length === 0 && !loading && (
|
||||
<div className={`empty-state ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
No shared files yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
19
frontend/src/components/ThemeToggle.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
|
||||
interface ThemeToggleProps {
|
||||
isDarkMode: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export const ThemeToggle: React.FC<ThemeToggleProps> = ({ isDarkMode, onToggle }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`theme-toggle ${isDarkMode ? 'theme-toggle-dark' : 'theme-toggle-light'}`}
|
||||
>
|
||||
<div className="theme-icon">
|
||||
{isDarkMode ? '☀️' : '🌙'}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
771
frontend/src/index.css
Normal file
|
@ -0,0 +1,771 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html, #root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Gradient Backgrounds */
|
||||
.gradient-bg {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.gradient-bg-dark {
|
||||
background: linear-gradient(135deg, #7b2ff7 0%, #f107a3 100%);
|
||||
}
|
||||
|
||||
.gradient-bg-light {
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
|
||||
}
|
||||
|
||||
/* Auth Container */
|
||||
.auth-container {
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
max-width: 28rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.auth-container-dark {
|
||||
background: rgba(31, 41, 55, 0.75);
|
||||
}
|
||||
|
||||
.auth-container-light {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
/* Auth Title */
|
||||
.auth-title {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Auth Form */
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Auth Inputs */
|
||||
.auth-input {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.auth-input:focus {
|
||||
ring: 2px;
|
||||
ring-offset: 2px;
|
||||
}
|
||||
|
||||
.auth-input-dark {
|
||||
background: rgba(55, 65, 81, 0.5);
|
||||
border-color: rgba(75, 85, 99, 1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.auth-input-dark::placeholder {
|
||||
color: rgba(156, 163, 175, 1);
|
||||
}
|
||||
|
||||
.auth-input-dark:focus {
|
||||
ring-color: rgba(147, 51, 234, 1);
|
||||
border-color: rgba(147, 51, 234, 1);
|
||||
}
|
||||
|
||||
.auth-input-light {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-color: rgba(209, 213, 219, 1);
|
||||
color: rgba(17, 24, 39, 1);
|
||||
}
|
||||
|
||||
.auth-input-light::placeholder {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
}
|
||||
|
||||
.auth-input-light:focus {
|
||||
ring-color: rgba(59, 130, 246, 1);
|
||||
border-color: rgba(59, 130, 246, 1);
|
||||
}
|
||||
|
||||
/* Auth Buttons */
|
||||
.auth-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.auth-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-button-dark {
|
||||
background: rgba(147, 51, 234, 1);
|
||||
}
|
||||
|
||||
.auth-button-dark:hover:not(:disabled) {
|
||||
background: rgba(126, 34, 206, 1);
|
||||
}
|
||||
|
||||
.auth-button-light {
|
||||
background: rgba(59, 130, 246, 1);
|
||||
}
|
||||
|
||||
.auth-button-light:hover:not(:disabled) {
|
||||
background: rgba(37, 99, 235, 1);
|
||||
}
|
||||
|
||||
/* Auth Switch */
|
||||
.auth-switch {
|
||||
background: none;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.auth-switch:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auth-switch-dark {
|
||||
color: rgba(196, 181, 253, 1);
|
||||
}
|
||||
|
||||
.auth-switch-dark:hover {
|
||||
color: rgba(221, 214, 254, 1);
|
||||
}
|
||||
|
||||
.auth-switch-light {
|
||||
color: rgba(59, 130, 246, 1);
|
||||
}
|
||||
|
||||
.auth-switch-light:hover {
|
||||
color: rgba(37, 99, 235, 1);
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.main-container {
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
max-width: 28rem;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.main-container-dark {
|
||||
background: rgba(31, 41, 55, 0.75);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.main-container-light {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
color: rgba(17, 24, 39, 1);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.main-subtitle {
|
||||
font-size: 1.125rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Logout Button */
|
||||
.logout-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(239, 68, 68, 1);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: rgba(220, 38, 38, 1);
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.theme-toggle-dark {
|
||||
background: rgba(75, 85, 99, 0.75);
|
||||
}
|
||||
|
||||
.theme-toggle-light {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.error-message {
|
||||
color: rgba(239, 68, 68, 1);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
background: rgba(254, 226, 226, 0.8);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: rgba(34, 197, 94, 1);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
background: rgba(220, 252, 231, 0.8);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
background: linear-gradient(135deg, #7b2ff7 0%, #f107a3 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ==== NEW MAIN APP LAYOUT STYLES ==== */
|
||||
|
||||
/* App Container */
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
/* App Layout */
|
||||
.app-layout {
|
||||
display: flex;
|
||||
height: calc(100vh - 2rem);
|
||||
gap: 10px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-dark {
|
||||
background: rgba(31, 41, 55, 0.75);
|
||||
}
|
||||
|
||||
.sidebar-light {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
/* Sidebar Title */
|
||||
.sidebar-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 2rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Sidebar Navigation */
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Navigation Buttons */
|
||||
.nav-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Active Navigation Buttons */
|
||||
.nav-button-active-dark {
|
||||
background: linear-gradient(135deg, rgba(147, 51, 234, 0.8), rgba(126, 34, 206, 0.8));
|
||||
color: white;
|
||||
box-shadow: 0 4px 20px rgba(147, 51, 234, 0.3);
|
||||
}
|
||||
|
||||
.nav-button-active-light {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(37, 99, 235, 0.8));
|
||||
color: white;
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Inactive Navigation Buttons */
|
||||
.nav-button-inactive-dark {
|
||||
background: rgba(55, 65, 81, 0.3);
|
||||
color: rgba(209, 213, 219, 1);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
|
||||
.nav-button-inactive-dark:hover {
|
||||
background: rgba(55, 65, 81, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-button-inactive-light {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
color: rgba(55, 65, 81, 1);
|
||||
border: 1px solid rgba(209, 213, 219, 0.3);
|
||||
}
|
||||
|
||||
.nav-button-inactive-light:hover {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: rgba(17, 24, 39, 1);
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
/* Logout Button in Sidebar */
|
||||
.logout-button-sidebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: rgba(239, 68, 68, 1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logout-button-sidebar:hover {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content-dark {
|
||||
background: rgba(31, 41, 55, 0.75);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.main-content-light {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
color: rgba(17, 24, 39, 1);
|
||||
}
|
||||
|
||||
/* Files Component */
|
||||
.files-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.files-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.files-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.files-table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Files Table */
|
||||
.files-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.files-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.files-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.files-table-dark th {
|
||||
background: rgba(55, 65, 81, 0.5);
|
||||
color: rgba(209, 213, 219, 1);
|
||||
}
|
||||
|
||||
.files-table-light th {
|
||||
background: rgba(249, 250, 251, 0.8);
|
||||
color: rgba(55, 65, 81, 1);
|
||||
}
|
||||
|
||||
.files-table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.files-table tbody tr {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.files-table-dark tbody tr:hover {
|
||||
background: rgba(55, 65, 81, 0.3);
|
||||
}
|
||||
|
||||
.files-table-light tbody tr:hover {
|
||||
background: rgba(249, 250, 251, 0.5);
|
||||
}
|
||||
|
||||
/* File Row Actions */
|
||||
.file-actions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-actions-button {
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-actions-button:hover {
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
}
|
||||
|
||||
.file-actions-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(209, 213, 219, 1);
|
||||
min-width: 120px;
|
||||
z-index: 20;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-actions-menu-dark {
|
||||
background: rgba(55, 65, 81, 1);
|
||||
border-color: rgba(75, 85, 99, 1);
|
||||
}
|
||||
|
||||
.file-actions-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-actions-menu-dark button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.file-actions-menu-dark button:hover {
|
||||
background: rgba(75, 85, 99, 1);
|
||||
}
|
||||
|
||||
.file-actions-menu button:hover {
|
||||
background: rgba(249, 250, 251, 1);
|
||||
}
|
||||
|
||||
.file-actions-menu button.delete-action {
|
||||
color: rgba(239, 68, 68, 1);
|
||||
}
|
||||
|
||||
.file-actions-menu button.delete-action:hover {
|
||||
background: rgba(254, 226, 226, 1);
|
||||
}
|
||||
|
||||
/* Upload Button */
|
||||
.upload-button {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.upload-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 15px 35px -5px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.upload-button-dark {
|
||||
background: linear-gradient(135deg, rgba(147, 51, 234, 0.9), rgba(126, 34, 206, 0.9));
|
||||
}
|
||||
|
||||
.upload-button-light {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(37, 99, 235, 0.9));
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.app-layout {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: calc(100vh - 2rem);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
order: 1;
|
||||
min-height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 1rem);
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.files-table th,
|
||||
.files-table td {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
10
frontend/src/main.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
1
frontend/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
8
frontend/tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
27
frontend/tsconfig.app.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
7
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
25
frontend/tsconfig.node.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
frontend/vite.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
43
lightcloud_app/.gitignore
vendored
|
@ -1,43 +0,0 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
|
@ -1,45 +0,0 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
- platform: android
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
- platform: ios
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
- platform: linux
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
- platform: macos
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
- platform: web
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
- platform: windows
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
@ -1,16 +0,0 @@
|
|||
# lightcloud
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
|
@ -1,31 +0,0 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
analyzer:
|
||||
errors:
|
||||
use_build_context_synchronously: ignore
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
13
lightcloud_app/android/.gitignore
vendored
|
@ -1,13 +0,0 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
|
@ -1,58 +0,0 @@
|
|||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file("local.properties")
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader("UTF-8") { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = "1"
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty("flutter.versionName")
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = "1.0"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.lightcloud.lightcloud"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.lightcloud.lightcloud"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutterVersionCode.toInteger()
|
||||
versionName = flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="lightcloud"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
|
@ -1,5 +0,0 @@
|
|||
package com.lightcloud.lightcloud
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
|
@ -1,5 +0,0 @@
|
|||
package com.lightcloud.lightcloud_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
Before Width: | Height: | Size: 544 B |
Before Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 721 B |
Before Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -1,7 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
|
@ -1,18 +0,0 @@
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = "../build"
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
|
@ -1,5 +0,0 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
|
@ -1,25 +0,0 @@
|
|||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
|
@ -1,9 +0,0 @@
|
|||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
class Config {
|
||||
static String get apiBaseUrl =>
|
||||
dotenv.env['API_URL'] ?? 'http://localhost:8082';
|
||||
static String get apiPath => '/api';
|
||||
|
||||
static String get apiUrl => '$apiBaseUrl$apiPath';
|
||||
}
|
|
@ -1,264 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'mainpage.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'config.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await dotenv.load();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<SharedPreferences>(
|
||||
future: SharedPreferences.getInstance(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
final prefs = snapshot.data!;
|
||||
final jwt = prefs.getString('jwt');
|
||||
return MaterialApp(
|
||||
title: 'Litecloud alpha',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color.fromARGB(255, 72, 4, 117)),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: jwt != null ? const MainPage() : const AuthScreen(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AuthScreen> createState() => _AuthScreenState();
|
||||
}
|
||||
|
||||
class _AuthScreenState extends State<AuthScreen> {
|
||||
bool _isLogin = true;
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmController = TextEditingController();
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
void _toggleAuthMode() {
|
||||
setState(() {
|
||||
_isLogin = !_isLogin;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
|
||||
if (!_isLogin && password != _confirmController.text.trim()) {
|
||||
setState(() {
|
||||
_error = "Le password non coincidono";
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final url =
|
||||
Uri.parse('${Config.apiUrl}/${_isLogin ? "login" : "register"}');
|
||||
|
||||
final body = jsonEncode({
|
||||
"username": email,
|
||||
"password": password,
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final data = jsonDecode(response.body);
|
||||
final token = data['token'];
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('jwt', token);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_error = "Credenziali non valide";
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = "Errore di rete";
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final deviceSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: deviceSize.width,
|
||||
height: deviceSize.height,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.purple, Color.fromARGB(255, 72, 4, 117)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
// Logo and tagline
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 40.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Litecloud',
|
||||
style: TextStyle(
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Rust based cloud storage',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Version 0.0.1 pre-alpha',
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Auth card
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 600.0),
|
||||
elevation: 8.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
_isLogin ? 'Login' : 'Register',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Username',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: Icon(Icons.lock),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
if (!_isLogin) const SizedBox(height: 16),
|
||||
if (!_isLogin)
|
||||
TextFormField(
|
||||
controller: _confirmController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirm Password',
|
||||
prefixIcon: Icon(Icons.lock),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: _loading ? null : _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
),
|
||||
child: _loading
|
||||
? const CircularProgressIndicator()
|
||||
: Text(
|
||||
_isLogin ? 'LOGIN' : 'REGISTER',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_error!,
|
||||
style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: _toggleAuthMode,
|
||||
child: Text(
|
||||
_isLogin
|
||||
? 'Create new account'
|
||||
: 'I already have an account',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,395 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'main.dart';
|
||||
import 'sidebar.dart';
|
||||
import 'shares.dart';
|
||||
import 'sharedialog.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'dart:io';
|
||||
import 'config.dart';
|
||||
|
||||
class FileInfo {
|
||||
final int id;
|
||||
final String name;
|
||||
final int size;
|
||||
final DateTime uploadedAt;
|
||||
|
||||
FileInfo({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.size,
|
||||
required this.uploadedAt,
|
||||
});
|
||||
|
||||
factory FileInfo.fromJson(Map<String, dynamic> json) {
|
||||
return FileInfo(
|
||||
id: json['id'],
|
||||
name: json['original_name'],
|
||||
size: json['size'],
|
||||
uploadedAt: DateTime.parse(json['uploaded_at']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({super.key});
|
||||
|
||||
@override
|
||||
State<MainPage> createState() => _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> {
|
||||
SidebarPage _selectedPage = SidebarPage.myFiles;
|
||||
List<FileInfo> _files = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchFiles();
|
||||
}
|
||||
|
||||
Future<void> _fetchFiles() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jwt = prefs.getString('jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
setState(() {
|
||||
_error = 'Sessione scaduta, effettua di nuovo il login.';
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.parse('${Config.apiUrl}/files');
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {'Authorization': 'Bearer $jwt'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
setState(() {
|
||||
_files = data.map((file) => FileInfo.fromJson(file)).toList();
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_error = 'Errore durante il recupero dei file.';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Errore di rete: ${e.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('jwt');
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const AuthScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPageSelected(SidebarPage page) {
|
||||
setState(() {
|
||||
_selectedPage = page;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _uploadFile() async {
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
if (result == null || result.files.isEmpty) return;
|
||||
|
||||
final file = result.files.first;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jwt = prefs.getString('jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Sessione scaduta, effettua di nuovo il login.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.parse('${Config.apiUrl}/upload');
|
||||
final request = http.MultipartRequest('POST', uri)
|
||||
..headers['Authorization'] = 'Bearer $jwt'
|
||||
..files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
file.bytes!,
|
||||
filename: file.name,
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Caricamento di ${file.name} in corso...')),
|
||||
);
|
||||
final response = await request.send();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('File caricato con successo!')),
|
||||
);
|
||||
_fetchFiles(); // Refresh the file list
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Errore durante l\'upload.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(FileInfo file) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jwt = prefs.getString('jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Sessione scaduta, effettua di nuovo il login.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.parse('${Config.apiUrl}/files/${file.id}');
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Download in corso...')),
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {'Authorization': 'Bearer $jwt'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await FileSaver.instance.saveFile(
|
||||
name: file.name,
|
||||
bytes: response.bodyBytes,
|
||||
);
|
||||
} else {
|
||||
// Desktop platforms
|
||||
final directory = await getDownloadsDirectory();
|
||||
if (directory != null) {
|
||||
final filePath = '${directory.path}/${file.name}';
|
||||
final fileObj = File(filePath);
|
||||
await fileObj.writeAsBytes(response.bodyBytes);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('File salvato in: $filePath')),
|
||||
);
|
||||
} else {
|
||||
throw Exception('Could not access downloads directory');
|
||||
}
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('File ${file.name} scaricato con successo!')),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Errore durante il download.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Errore di rete: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteFile(FileInfo file) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jwt = prefs.getString('jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Sessione scaduta, effettua di nuovo il login.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.parse('${Config.apiUrl}/files/${file.id}');
|
||||
final response = await http.delete(
|
||||
uri,
|
||||
headers: {'Authorization': 'Bearer $jwt'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 204) {
|
||||
setState(() {
|
||||
_files.removeWhere((f) => f.id == file.id);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('File ${file.name} eliminato con successo!')),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Errore durante l\'eliminazione.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Errore di rete: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showShareDialog(FileInfo file) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ShareDialog(
|
||||
fileId: file.id,
|
||||
fileName: file.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFileOptions(FileInfo file) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: const Text('Download'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_downloadFile(file);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Condividi'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showShareDialog(file);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.red[700]),
|
||||
title: Text('Elimina', style: TextStyle(color: Colors.red[700])),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_deleteFile(file);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMyFiles() {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_error!, style: TextStyle(color: Colors.red[700])),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _fetchFiles,
|
||||
child: const Text('Riprova'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_files.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Nessun file trovato. Carica il tuo primo file!'),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(32),
|
||||
itemCount: _files.length,
|
||||
separatorBuilder: (_, __) => const Divider(),
|
||||
itemBuilder: (context, idx) {
|
||||
final file = _files[idx];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
title: Text(file.name),
|
||||
subtitle: Text(
|
||||
"Size: ${(file.size / 1024).toStringAsFixed(1)} KB • Uploaded: ${file.uploadedAt.toLocal()}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () => _showFileOptions(file),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content;
|
||||
if (_selectedPage == SidebarPage.myFiles) {
|
||||
content = Stack(
|
||||
children: [
|
||||
_buildMyFiles(),
|
||||
Positioned(
|
||||
bottom: 32,
|
||||
right: 32,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _uploadFile,
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text("Upload"),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (_selectedPage == SidebarPage.shared) {
|
||||
content = const SharesPage();
|
||||
} else {
|
||||
content = const Center(child: Text("Settings (coming soon)"));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
Sidebar(
|
||||
selectedPage: _selectedPage,
|
||||
onPageSelected: _onPageSelected,
|
||||
onLogout: _logout,
|
||||
),
|
||||
Expanded(child: content),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'config.dart';
|
||||
|
||||
class ShareDialog extends StatefulWidget {
|
||||
final int fileId;
|
||||
final String fileName;
|
||||
|
||||
const ShareDialog({
|
||||
required this.fileId,
|
||||
required this.fileName,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ShareDialog> createState() => _ShareDialogState();
|
||||
}
|
||||
|
||||
class _ShareDialogState extends State<ShareDialog> {
|
||||
bool _isPublicShare = true;
|
||||
int _expirationDays = 7;
|
||||
bool _isLoading = false;
|
||||
String? shareLink;
|
||||
String? _error;
|
||||
|
||||
Future<void> _createShare() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jwt = prefs.getString('jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
setState(() {
|
||||
_error = 'Sessione scaduta, effettua di nuovo il login.';
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.parse('${Config.apiUrl}/share');
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $jwt',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'file_id': widget.fileId,
|
||||
'expires_in_days': _isPublicShare ? _expirationDays : null,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final shareId = jsonDecode(response.body);
|
||||
setState(() {
|
||||
shareLink = '${Config.apiUrl}/shared/$shareId';
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_error = 'Errore durante la creazione del link di condivisione.';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Errore di rete: ${e.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Condividi "${widget.fileName}"'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Link pubblico'),
|
||||
subtitle: const Text('Chiunque con il link può accedere al file'),
|
||||
value: _isPublicShare,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isPublicShare = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_isPublicShare) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text('Scadenza del link:'),
|
||||
Slider(
|
||||
value: _expirationDays.toDouble(),
|
||||
min: 1,
|
||||
max: 30,
|
||||
divisions: 29,
|
||||
label: _expirationDays.toString(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_expirationDays = value.round();
|
||||
});
|
||||
},
|
||||
),
|
||||
Text('Il link scadrà dopo $_expirationDays giorni'),
|
||||
],
|
||||
if (shareLink != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text('Link di condivisione:'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
shareLink!,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
tooltip: 'Copia link',
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: shareLink ?? ''));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Link copiato negli appunti')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Chiudi'),
|
||||
),
|
||||
if (shareLink == null)
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _createShare,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Crea link'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class SharedFile {
|
||||
final String id;
|
||||
final int fileId;
|
||||
final String fileName;
|
||||
final DateTime createdAt;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
SharedFile({
|
||||
required this.id,
|
||||
required this.fileId,
|
||||
required this.fileName,
|
||||
required this.createdAt,
|
||||
this.expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
class SharesPage extends StatefulWidget {
|
||||
const SharesPage({super.key});
|
||||
|
||||
@override
|
||||
State<SharesPage> createState() => _SharesPageState();
|
||||
}
|
||||
|
||||
class _SharesPageState extends State<SharesPage> {
|
||||
List<SharedFile> _sharedFiles = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchSharedFiles();
|
||||
}
|
||||
|
||||
Future<void> _fetchSharedFiles() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jwt = prefs.getString('jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
setState(() {
|
||||
_error = 'Sessione scaduta, effettua di nuovo il login.';
|
||||
_isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.parse('http://localhost:8082/api/shares');
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {'Authorization': 'Bearer $jwt'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
setState(() {
|
||||
_sharedFiles = data
|
||||
.map((share) => SharedFile(
|
||||
id: share['id'],
|
||||
fileId: share['file_id'],
|
||||
fileName: share['file_name'],
|
||||
createdAt: DateTime.parse(share['created_at']),
|
||||
expiresAt: share['expires_at'] != null
|
||||
? DateTime.parse(share['expires_at'])
|
||||
: null,
|
||||
))
|
||||
.toList();
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_error = 'Errore durante il recupero dei file condivisi.';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Errore di rete: ${e.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadSharedFile(SharedFile share) async {
|
||||
try {
|
||||
final shareLink = 'http://localhost:8082/api/shared/${share.id}';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Download in corso...')),
|
||||
);
|
||||
|
||||
final response = await http.get(Uri.parse(shareLink));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Save the file to disk
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
// Mobile platforms
|
||||
await FileSaver.instance.saveFile(
|
||||
name: share.fileName,
|
||||
bytes: response.bodyBytes,
|
||||
);
|
||||
} else {
|
||||
// Desktop platforms
|
||||
final directory = await getDownloadsDirectory();
|
||||
if (directory != null) {
|
||||
final filePath = '${directory.path}/${share.fileName}';
|
||||
final fileObj = File(filePath);
|
||||
await fileObj.writeAsBytes(response.bodyBytes);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('File salvato in: $filePath')),
|
||||
);
|
||||
} else {
|
||||
throw Exception('Could not access downloads directory');
|
||||
}
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('File ${share.fileName} scaricato con successo!')),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Errore durante il download.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Errore di rete: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_error!, style: TextStyle(color: Colors.red[700])),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _fetchSharedFiles,
|
||||
child: const Text('Riprova'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_sharedFiles.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Nessun file condiviso. Condividi un file dalla sezione "My Files"!'),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(32),
|
||||
itemCount: _sharedFiles.length,
|
||||
separatorBuilder: (_, __) => const Divider(),
|
||||
itemBuilder: (context, idx) {
|
||||
final share = _sharedFiles[idx];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.link),
|
||||
title: Text(share.fileName),
|
||||
subtitle: Text(
|
||||
"Creato: ${share.createdAt.toLocal()}${share.expiresAt != null ? ' • Scade: ${share.expiresAt!.toLocal()}' : ' • Non scade mai'}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
tooltip: 'Scarica file',
|
||||
onPressed: () => _downloadSharedFile(share),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
tooltip: 'Copia link',
|
||||
onPressed: () {
|
||||
final shareLink =
|
||||
'http://localhost:8082/api/shared/${share.id}';
|
||||
Clipboard.setData(ClipboardData(text: shareLink));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Link copiato negli appunti')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SidebarPage { myFiles, shared, settings }
|
||||
|
||||
class Sidebar extends StatelessWidget {
|
||||
final SidebarPage selectedPage;
|
||||
final ValueChanged<SidebarPage> onPageSelected;
|
||||
final VoidCallback onLogout;
|
||||
|
||||
const Sidebar({
|
||||
required this.selectedPage,
|
||||
required this.onPageSelected,
|
||||
required this.onLogout,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor =
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.15);
|
||||
final outlineColor = Colors.purple[100];
|
||||
|
||||
Widget buildTile(
|
||||
{required IconData icon,
|
||||
required String label,
|
||||
required SidebarPage page}) {
|
||||
final selected = selectedPage == page;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
decoration: selected
|
||||
? BoxDecoration(
|
||||
color: selectedColor,
|
||||
border: Border.all(color: outlineColor!, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
: null,
|
||||
child: ListTile(
|
||||
leading: Icon(icon,
|
||||
color: selected ? Theme.of(context).colorScheme.primary : null),
|
||||
title: Text(label,
|
||||
style: TextStyle(
|
||||
color:
|
||||
selected ? Theme.of(context).colorScheme.primary : null)),
|
||||
onTap: () => onPageSelected(page),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return NavigationDrawer(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
buildTile(
|
||||
icon: Icons.folder, label: 'My Files', page: SidebarPage.myFiles),
|
||||
buildTile(
|
||||
icon: Icons.share, label: 'Shared Files', page: SidebarPage.shared),
|
||||
buildTile(
|
||||
icon: Icons.settings,
|
||||
label: 'Settings',
|
||||
page: SidebarPage.settings),
|
||||
const Spacer(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Logout'),
|
||||
onTap: onLogout,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
name: lightcloud
|
||||
description: "A new Flutter project."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.4.3 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.6
|
||||
shared_preferences: ^2.3.3
|
||||
http: ^1.4.0
|
||||
file_picker: ^8.3.2
|
||||
path_provider: ^2.1.5
|
||||
file_saver: ^0.2.14
|
||||
flutter_dotenv: ^5.2.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter_lints: ^3.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- .env
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
@ -1,30 +0,0 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:lightcloud/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 20 KiB |
|
@ -1,38 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="lightcloud">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>lightcloud</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"name": "lightcloud",
|
||||
"short_name": "lightcloud",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
17
lightcloud_app/windows/.gitignore
vendored
|
@ -1,17 +0,0 @@
|
|||
flutter/ephemeral/
|
||||
|
||||
# Visual Studio user-specific files.
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Visual Studio build-related files.
|
||||
x64/
|
||||
x86/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
|
@ -1,108 +0,0 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(lightcloud_app LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "lightcloud_app")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(VERSION 3.14...3.25)
|
||||
|
||||
# Define build configuration option.
|
||||
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(IS_MULTICONFIG)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||
CACHE STRING "" FORCE)
|
||||
else()
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
endif()
|
||||
# Define settings for the Profile build mode.
|
||||
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
|
||||
# Use Unicode for all projects.
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# Support files are copied into place next to the executable, so that it can
|
||||
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||
# so that building and running from within Visual Studio will work.
|
||||
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||
# Make the "install" step default, as it's required to run.
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
CONFIGURATIONS Profile;Release
|
||||
COMPONENT Runtime)
|
|
@ -1,109 +0,0 @@
|
|||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||
|
||||
# Set fallback configurations for older versions of the flutter tool.
|
||||
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||
endif()
|
||||
|
||||
# === Flutter Library ===
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"flutter_export.h"
|
||||
"flutter_windows.h"
|
||||
"flutter_messenger.h"
|
||||
"flutter_plugin_registrar.h"
|
||||
"flutter_texture_registrar.h"
|
||||
)
|
||||
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Wrapper ===
|
||||
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||
"core_implementations.cc"
|
||||
"standard_codec.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||
"plugin_registrar.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||
"flutter_engine.cc"
|
||||
"flutter_view_controller.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||
|
||||
# Wrapper sources needed for a plugin.
|
||||
add_library(flutter_wrapper_plugin STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_plugin)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden)
|
||||
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||
|
||||
# Wrapper sources needed for the runner.
|
||||
add_library(flutter_wrapper_app STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_app)
|
||||
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_app PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
${PHONY_OUTPUT}
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
|
@ -1,14 +0,0 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSaverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter/plugin_registry.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
|
@ -1,24 +0,0 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_saver
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
|
@ -1,40 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the build version.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
|
||||
# Add dependency libraries and include directories. Add any application-specific
|
||||
# dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
|
@ -1,121 +0,0 @@
|
|||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#pragma code_page(65001)
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (United States) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0,0
|
||||
#endif
|
||||
|
||||
#if defined(FLUTTER_VERSION)
|
||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VERSION_AS_NUMBER
|
||||
PRODUCTVERSION VERSION_AS_NUMBER
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS__WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.lightcloud" "\0"
|
||||
VALUE "FileDescription", "lightcloud_app" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "lightcloud_app" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2025 com.lightcloud. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "lightcloud_app.exe" "\0"
|
||||
VALUE "ProductName", "lightcloud_app" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
|
@ -1,71 +0,0 @@
|
|||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||
: project_(project) {}
|
||||
|
||||
FlutterWindow::~FlutterWindow() {}
|
||||
|
||||
bool FlutterWindow::OnCreate() {
|
||||
if (!Win32Window::OnCreate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
// The size here must match the window dimensions to avoid unnecessary surface
|
||||
// creation / destruction in the startup path.
|
||||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||
// Ensure that basic setup of the controller was successful.
|
||||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||
return false;
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
this->Show();
|
||||
});
|
||||
|
||||
// Flutter can complete the first frame before the "show window" callback is
|
||||
// registered. The following call ensures a frame is pending to ensure the
|
||||
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||
flutter_controller_->ForceRedraw();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FlutterWindow::OnDestroy() {
|
||||
if (flutter_controller_) {
|
||||
flutter_controller_ = nullptr;
|
||||
}
|
||||
|
||||
Win32Window::OnDestroy();
|
||||
}
|
||||
|
||||
LRESULT
|
||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||
if (flutter_controller_) {
|
||||
std::optional<LRESULT> result =
|
||||
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||
lparam);
|
||||
if (result) {
|
||||
return *result;
|
||||
}
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case WM_FONTCHANGE:
|
||||
flutter_controller_->engine()->ReloadSystemFonts();
|
||||
break;
|
||||
}
|
||||
|
||||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||
#define RUNNER_FLUTTER_WINDOW_H_
|
||||
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "win32_window.h"
|
||||
|
||||
// A window that does nothing but host a Flutter view.
|
||||
class FlutterWindow : public Win32Window {
|
||||
public:
|
||||
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||
explicit FlutterWindow(const flutter::DartProject& project);
|
||||
virtual ~FlutterWindow();
|
||||
|
||||
protected:
|
||||
// Win32Window:
|
||||
bool OnCreate() override;
|
||||
void OnDestroy() override;
|
||||
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept override;
|
||||
|
||||
private:
|
||||
// The project to run.
|
||||
flutter::DartProject project_;
|
||||
|
||||
// The Flutter instance hosted by this window.
|
||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||
};
|
||||
|
||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
|
@ -1,43 +0,0 @@
|
|||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
CreateAndAttachConsole();
|
||||
}
|
||||
|
||||
// Initialize COM, so that it is available for use in the library and/or
|
||||
// plugins.
|
||||
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
flutter::DartProject project(L"data");
|
||||
|
||||
std::vector<std::string> command_line_arguments =
|
||||
GetCommandLineArguments();
|
||||
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"lightcloud_app", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
::CoUninitialize();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by Runner.rc
|
||||
//
|
||||
#define IDI_APP_ICON 101
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
Before Width: | Height: | Size: 33 KiB |
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
|
@ -1,65 +0,0 @@
|
|||
#include "utils.h"
|
||||
|
||||
#include <flutter_windows.h>
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void CreateAndAttachConsole() {
|
||||
if (::AllocConsole()) {
|
||||
FILE *unused;
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||
_dup2(_fileno(stdout), 1);
|
||||
}
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||
_dup2(_fileno(stdout), 2);
|
||||
}
|
||||
std::ios::sync_with_stdio();
|
||||
FlutterDesktopResyncOutputStreams();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> GetCommandLineArguments() {
|
||||
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||
int argc;
|
||||
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||
if (argv == nullptr) {
|
||||
return std::vector<std::string>();
|
||||
}
|
||||
|
||||
std::vector<std::string> command_line_arguments;
|
||||
|
||||
// Skip the first argument as it's the binary name.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||
}
|
||||
|
||||
::LocalFree(argv);
|
||||
|
||||
return command_line_arguments;
|
||||
}
|
||||
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||
if (utf16_string == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
unsigned int target_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
-1, nullptr, 0, nullptr, nullptr)
|
||||
-1; // remove the trailing null character
|
||||
int input_length = (int)wcslen(utf16_string);
|
||||
std::string utf8_string;
|
||||
if (target_length == 0 || target_length > utf8_string.max_size()) {
|
||||
return utf8_string;
|
||||
}
|
||||
utf8_string.resize(target_length);
|
||||
int converted_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||
if (converted_length == 0) {
|
||||
return std::string();
|
||||
}
|
||||
return utf8_string;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#ifndef RUNNER_UTILS_H_
|
||||
#define RUNNER_UTILS_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Creates a console for the process, and redirects stdout and stderr to
|
||||
// it for both the runner and the Flutter library.
|
||||
void CreateAndAttachConsole();
|
||||
|
||||
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||
|
||||
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||
std::vector<std::string> GetCommandLineArguments();
|
||||
|
||||
#endif // RUNNER_UTILS_H_
|
|
@ -1,288 +0,0 @@
|
|||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
/// Window attribute that enables dark mode window decorations.
|
||||
///
|
||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||
/// version 10.0.22000.0.
|
||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
/// Registry key for app theme preference.
|
||||
///
|
||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||
/// value indicates apps should use light mode.
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
}
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registrar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
RegisterClass(&window_class);
|
||||
class_registered_ = true;
|
||||
}
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::Create(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
static_cast<LONG>(origin.y)};
|
||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTheme(window);
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
bool Win32Window::Show() {
|
||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||
UpdateTheme(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
void Win32Window::UpdateTheme(HWND const window) {
|
||||
DWORD light_mode;
|
||||
DWORD light_mode_size = sizeof(light_mode);
|
||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||
kGetPreferredBrightnessRegValue,
|
||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||
&light_mode_size);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
BOOL enable_dark_mode = light_mode == 0;
|
||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
: width(width), height(height) {}
|
||||
};
|
||||
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates a win32 window with |title| that is positioned and sized using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size this function will scale the inputted width and height as
|
||||
// as appropriate for the default monitor. The window is invisible until
|
||||
// |Show| is called. Returns true if the window was created successfully.
|
||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||
|
||||
// Show the current window. Returns true if the window was successfully shown.
|
||||
bool Show();
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
|
||||
// Inserts |content| into the window tree.
|
||||
void SetChildContent(HWND content);
|
||||
|
||||
// Returns the backing Window handle to enable clients to set icon and other
|
||||
// window properties. Returns nullptr if the window has been destroyed.
|
||||
HWND GetHandle();
|
||||
|
||||
// If true, closing this window will quit the application.
|
||||
void SetQuitOnClose(bool quit_on_close);
|
||||
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
virtual LRESULT MessageHandler(HWND window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Called when CreateAndShow is called, allowing subclass window-related
|
||||
// setup. Subclasses should return false if setup fails.
|
||||
virtual bool OnCreate();
|
||||
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
// Update the window frame's theme to match the system theme.
|
||||
static void UpdateTheme(HWND const window);
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
// window handle for top level window.
|
||||
HWND window_handle_ = nullptr;
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|