Compare commits
2 commits
37f0c72114
...
315746852b
Author | SHA1 | Date | |
---|---|---|---|
|
315746852b | ||
|
ca9f143b30 |
|
@ -22,6 +22,7 @@ rocket_cors = "0.6"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
password-hash = "0.5"
|
password-hash = "0.5"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
|
utoipa = "5.3.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
|
|
137
api/src/file.rs
|
@ -15,8 +15,9 @@ use crate::encryption::{encrypt_data, decrypt_data};
|
||||||
use crate::models::{File, Share};
|
use crate::models::{File, Share};
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
pub struct Upload<'r> {
|
pub struct Upload<'f> {
|
||||||
file: TempFile<'r>,
|
pub file: TempFile<'f>,
|
||||||
|
pub filename: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/upload", data = "<upload>", format = "multipart/form-data")]
|
#[post("/upload", data = "<upload>", format = "multipart/form-data")]
|
||||||
|
@ -26,32 +27,28 @@ pub async fn upload_file(
|
||||||
upload: Form<Upload<'_>>
|
upload: Form<Upload<'_>>
|
||||||
) -> Result<Status, Status> {
|
) -> Result<Status, Status> {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
if let Err(_) = {
|
let mut file = upload.file.open().await.map_err(|_| Status::BadRequest)?;
|
||||||
let mut file = match upload.file.open().await {
|
file.read_to_end(&mut buffer).await.map_err(|_| Status::BadRequest)?;
|
||||||
Ok(file) => file,
|
|
||||||
Err(_) => return Err(Status::BadRequest),
|
|
||||||
};
|
|
||||||
file.read_to_end(&mut buffer).await
|
|
||||||
} {
|
|
||||||
return Err(Status::BadRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
let nonce = rand::random::<[u8; 12]>();
|
let nonce = rand::random::<[u8; 12]>();
|
||||||
let key = std::env::var("ENCRYPTION_KEY").expect("ENCRYPTION_KEY must be set");
|
let key = std::env::var("ENCRYPTION_KEY").expect("ENCRYPTION_KEY must be set");
|
||||||
let encrypted = encrypt_data(&buffer, key.as_bytes(), &nonce);
|
let encrypted = encrypt_data(&buffer, key.as_bytes(), &nonce);
|
||||||
|
|
||||||
let file_id = Uuid::new_v4().to_string();
|
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::create_dir_all(&user_dir).await.map_err(|_| Status::InternalServerError)?;
|
||||||
fs::write(format!("{}.nonce", &storage_path), &nonce).await.ok();
|
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;
|
let size = buffer.len() as i64;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO files (user_id, original_name, storage_path, uploaded_at, size) VALUES ($1, $2, $3, $4, $5)",
|
"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,
|
original_name,
|
||||||
storage_path,
|
storage_path,
|
||||||
Utc::now(),
|
Utc::now(),
|
||||||
|
@ -64,6 +61,7 @@ pub async fn upload_file(
|
||||||
Ok(Status::Created)
|
Ok(Status::Created)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[get("/files")]
|
#[get("/files")]
|
||||||
pub async fn list_user_files(pool: &State<PgPool>, user: AuthenticatedUser) -> Json<Vec<File>> {
|
pub async fn list_user_files(pool: &State<PgPool>, user: AuthenticatedUser) -> Json<Vec<File>> {
|
||||||
let Some(user_id) = user.user_id else {
|
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
|
.await
|
||||||
.map_err(|_| Status::InternalServerError)?;
|
.map_err(|_| Status::InternalServerError)?;
|
||||||
|
|
||||||
let share = sqlx::query_as::<_, Share>(
|
Ok(Json(id.to_string()))
|
||||||
"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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/shared/<link>")]
|
#[get("/shared/<link>")]
|
||||||
|
@ -240,4 +230,101 @@ pub async fn download_shared(pool: &State<PgPool>, link: &str) -> Option<FileDow
|
||||||
data: decrypted,
|
data: decrypted,
|
||||||
filename: file.original_name,
|
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::response::status::Custom;
|
||||||
use rocket::data::ByteUnit;
|
use rocket::data::ByteUnit;
|
||||||
|
|
||||||
|
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
|
||||||
use rocket_cors::{CorsOptions, AllowedOrigins};
|
use rocket_cors::{CorsOptions};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod encryption;
|
mod encryption;
|
||||||
|
@ -67,7 +68,9 @@ async fn main() -> Result<(), rocket::Error> {
|
||||||
download_file,
|
download_file,
|
||||||
download_shared,
|
download_shared,
|
||||||
list_user_files,
|
list_user_files,
|
||||||
|
list_user_shares,
|
||||||
delete_file,
|
delete_file,
|
||||||
|
delete_share,
|
||||||
share_file,
|
share_file,
|
||||||
update_role,
|
update_role,
|
||||||
update_quota,
|
update_quota,
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub struct File {
|
||||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
pub struct Share {
|
pub struct Share {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub file_id: Option<i32>, // Changed to Option<i32>
|
pub file_id: Option<i32>,
|
||||||
pub shared_by: i32,
|
pub shared_by: i32,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub expires_at: Option<chrono::NaiveDateTime>,
|
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_
|
|