Compare commits
10 Commits
4ffb4b2855
...
4d564c1d11
Author | SHA1 | Date | |
---|---|---|---|
4d564c1d11 | |||
2af2e83164 | |||
f6af87dc27 | |||
114a34de8f | |||
f8c46796ae | |||
68d88469c1 | |||
9bb1e2add4 | |||
617df25581 | |||
80d98cade6 | |||
0c81399ea1 |
@ -2,6 +2,15 @@
|
||||
|
||||
AURCache is a build server and repository for Archlinux packages sourced from the AUR (Arch User Repository). It features a Flutter frontend and Rust backend, enabling users to add packages for building and subsequently serves them as a pacman repository. Notably, AURCache automatically detects when a package is out of date and displays it within the frontend.
|
||||
|
||||
<p><img src="res/imgs/screenshot1.png" alt=""/>
|
||||
|
||||
<details>
|
||||
<summary>More Images:</summary>
|
||||
<br>
|
||||
<img src="res/imgs/screenshot2.png" alt=""/>
|
||||
<img src="res/imgs/screenshot3.png" alt=""/></p>
|
||||
</details>
|
||||
|
||||
## Deployment with Docker and Docker-compose
|
||||
|
||||
To deploy AURCache using Docker and Docker-compose, you can use the following example docker-compose.yml file:
|
||||
|
@ -19,6 +19,7 @@ pub fn build_api() -> Vec<Route> {
|
||||
stats,
|
||||
get_build,
|
||||
get_package,
|
||||
package_update_endpoint
|
||||
package_update_endpoint,
|
||||
cancel_build
|
||||
]
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use crate::db::prelude::Builds;
|
||||
use crate::db::{builds, packages, versions};
|
||||
use rocket::response::status::NotFound;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{delete, get, State};
|
||||
use rocket::{delete, get, post, State};
|
||||
|
||||
use crate::api::types::input::ListBuildsModel;
|
||||
use rocket_okapi::openapi;
|
||||
@ -12,6 +12,8 @@ use sea_orm::QueryFilter;
|
||||
use sea_orm::{
|
||||
DatabaseConnection, EntityTrait, ModelTrait, QueryOrder, QuerySelect, RelationTrait,
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use crate::builder::types::Action;
|
||||
|
||||
#[openapi(tag = "build")]
|
||||
#[get("/build/<buildid>/output?<startline>")]
|
||||
@ -142,3 +144,17 @@ pub async fn delete_build(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[openapi(tag = "build")]
|
||||
#[post("/build/<buildid>/cancel")]
|
||||
pub async fn cancel_build(
|
||||
db: &State<DatabaseConnection>,
|
||||
tx: &State<Sender<Action>>,
|
||||
buildid: i32,
|
||||
) -> Result<(), NotFound<String>> {
|
||||
let db = db as &DatabaseConnection;
|
||||
|
||||
let _ = tx.send(Action::Cancel(buildid)).map_err(|e| NotFound(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -53,7 +53,7 @@ async fn get_stats(db: &DatabaseConnection) -> anyhow::Result<ListStats> {
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct BuildTimeStruct {
|
||||
avg_build_time: f64,
|
||||
avg_build_time: Option<f64>,
|
||||
}
|
||||
|
||||
let unique: BuildTimeStruct =
|
||||
@ -68,7 +68,7 @@ async fn get_stats(db: &DatabaseConnection) -> anyhow::Result<ListStats> {
|
||||
.await?
|
||||
.ok_or(anyhow::anyhow!("No Average build time"))?;
|
||||
|
||||
let avg_build_time: u32 = unique.avg_build_time as u32;
|
||||
let avg_build_time: u32 = unique.avg_build_time.unwrap_or(0.0) as u32;
|
||||
|
||||
// Count total packages
|
||||
let total_packages: u32 = Packages::find().count(db).await?.try_into()?;
|
||||
|
@ -43,7 +43,7 @@ pub async fn get_info_by_name(pkg_name: &str) -> anyhow::Result<Package> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn download_pkgbuild(url: &str, dest_dir: &str) -> anyhow::Result<String> {
|
||||
pub async fn download_pkgbuild(url: &str, dest_dir: &str, clear_build_dir: bool) -> anyhow::Result<String> {
|
||||
let (file_data, file_name) = match download_file(url).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
@ -51,6 +51,10 @@ pub async fn download_pkgbuild(url: &str, dest_dir: &str) -> anyhow::Result<Stri
|
||||
}
|
||||
};
|
||||
|
||||
if clear_build_dir {
|
||||
fs::remove_dir_all(dest_dir)?;
|
||||
}
|
||||
|
||||
// Check if the directory exists
|
||||
if fs::metadata(dest_dir).is_err() {
|
||||
// Create the directory if it does not exist
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Add;
|
||||
use crate::builder::types::Action;
|
||||
use crate::db::builds::ActiveModel;
|
||||
use crate::db::prelude::{Builds, Packages};
|
||||
@ -5,15 +7,16 @@ use crate::db::{builds, packages, versions};
|
||||
use crate::repo::repo::add_pkg;
|
||||
use anyhow::anyhow;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set};
|
||||
use std::ops::Add;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use tokio::sync::{broadcast, Semaphore};
|
||||
use tokio::sync::{broadcast, Mutex, Semaphore};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub async fn init(db: DatabaseConnection, tx: Sender<Action>) {
|
||||
let semaphore = Arc::new(Semaphore::new(1));
|
||||
let job_handles: Arc<Mutex<HashMap<i32, JoinHandle<_>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
loop {
|
||||
if let Ok(_result) = tx.subscribe().recv().await {
|
||||
@ -28,13 +31,42 @@ pub async fn init(db: DatabaseConnection, tx: Sender<Action>) {
|
||||
build_model,
|
||||
db.clone(),
|
||||
semaphore.clone(),
|
||||
job_handles.clone()
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Action::Cancel(build_id) => {
|
||||
let _ = cancel_build(build_id, job_handles.clone(), db.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cancel_build(build_id: i32, job_handles: Arc<Mutex<HashMap<i32, JoinHandle<()>>>>, db: DatabaseConnection) -> anyhow::Result<()> {
|
||||
let build = Builds::find_by_id(build_id)
|
||||
.one(&db)
|
||||
.await?
|
||||
.ok_or(anyhow!("No build found"))?;
|
||||
|
||||
let mut build: builds::ActiveModel = build.into();
|
||||
build.status = Set(Some(4));
|
||||
build.end_time = Set(Some(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as u32,
|
||||
));
|
||||
let _ = build.clone().update(&db).await;
|
||||
|
||||
job_handles
|
||||
.lock()
|
||||
.await
|
||||
.remove(&build.id.clone().unwrap())
|
||||
.ok_or(anyhow!("No build found"))?
|
||||
.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn queue_package(
|
||||
name: String,
|
||||
@ -44,12 +76,14 @@ async fn queue_package(
|
||||
mut build_model: builds::ActiveModel,
|
||||
db: DatabaseConnection,
|
||||
semaphore: Arc<Semaphore>,
|
||||
job_handles: Arc<Mutex<HashMap<i32, JoinHandle<()>>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let permits = Arc::clone(&semaphore);
|
||||
let build_id = build_model.id.clone().unwrap();
|
||||
|
||||
// spawn new thread for each pkg build
|
||||
// todo add queue and build two packages in parallel
|
||||
tokio::spawn(async move {
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permits.acquire().await.unwrap();
|
||||
|
||||
// set build status to building
|
||||
@ -58,6 +92,7 @@ async fn queue_package(
|
||||
|
||||
let _ = build_package(build_model, db, version_model, version, name, url).await;
|
||||
});
|
||||
job_handles.lock().await.insert(build_id, handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -83,7 +118,7 @@ async fn build_package(
|
||||
pkg.status = Set(0);
|
||||
pkg = pkg.update(&db).await?.into();
|
||||
|
||||
match add_pkg(url, version, name, tx).await {
|
||||
match add_pkg(url, version, name, tx, false).await {
|
||||
Ok(pkg_file_name) => {
|
||||
println!("successfully built package");
|
||||
// update package success status
|
||||
|
@ -9,4 +9,5 @@ pub enum Action {
|
||||
versions::ActiveModel,
|
||||
builds::ActiveModel,
|
||||
),
|
||||
Cancel(i32),
|
||||
}
|
||||
|
@ -20,8 +20,9 @@ pub async fn add_pkg(
|
||||
version: String,
|
||||
name: String,
|
||||
tx: Sender<String>,
|
||||
clear_build_dir: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
let fname = download_pkgbuild(format!("{}{}", BASEURL, url).as_str(), "./builds").await?;
|
||||
let fname = download_pkgbuild(format!("{}{}", BASEURL, url).as_str(), "./builds", clear_build_dir).await?;
|
||||
let pkg_file_names = build_pkgbuild(
|
||||
format!("./builds/{fname}"),
|
||||
version.as_str(),
|
||||
|
@ -1,3 +1,4 @@
|
||||
use std::env;
|
||||
use crate::db::packages;
|
||||
use crate::db::prelude::{Packages, Versions};
|
||||
use anyhow::anyhow;
|
||||
@ -5,11 +6,16 @@ use aur_rs::{Package, Request};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::{sleep};
|
||||
|
||||
pub fn start_aur_version_checking(db: DatabaseConnection) {
|
||||
let default_version_check_interval = 10;
|
||||
let check_interval = env::var("VERSION_CHECK_INTERVAL")
|
||||
.map(|x| x.parse::<u64>().unwrap_or(default_version_check_interval))
|
||||
.unwrap_or(default_version_check_interval);
|
||||
|
||||
tokio::spawn(async move {
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
sleep(Duration::from_secs(check_interval)).await;
|
||||
loop {
|
||||
println!("performing aur version checks");
|
||||
match aur_check_versions(db.clone()).await {
|
||||
|
@ -21,12 +21,17 @@ extension BuildsAPI on ApiClient {
|
||||
}
|
||||
|
||||
Future<Build> getBuild(int id) async {
|
||||
final resp = await getRawClient().get("/build/${id}");
|
||||
final resp = await getRawClient().get("/build/$id");
|
||||
return Build.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<bool> deleteBuild(int id) async {
|
||||
final resp = await getRawClient().delete("/build/${id}");
|
||||
final resp = await getRawClient().delete("/build/$id");
|
||||
return resp.statusCode == 400;
|
||||
}
|
||||
|
||||
Future<bool> cancelBuild(int id) async {
|
||||
final resp = await getRawClient().post("/build/$id/cancel");
|
||||
return resp.statusCode == 400;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../constants/color_constants.dart';
|
||||
import '../models/build.dart';
|
||||
import 'dashboard/your_packages.dart';
|
||||
import '../utils/package_color.dart';
|
||||
|
||||
class BuildsTable extends StatelessWidget {
|
||||
const BuildsTable({super.key, required this.data});
|
||||
|
@ -3,15 +3,15 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class BuildsChart extends StatefulWidget {
|
||||
const BuildsChart({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.nrbuilds,
|
||||
required this.nrfailedbuilds,
|
||||
required this.nrActiveBuilds,
|
||||
}) : super(key: key);
|
||||
required this.nrEnqueuedBuilds,
|
||||
});
|
||||
|
||||
final int nrbuilds;
|
||||
final int nrfailedbuilds;
|
||||
final int nrActiveBuilds;
|
||||
final int nrEnqueuedBuilds;
|
||||
|
||||
@override
|
||||
_BuildsChartState createState() => _BuildsChartState();
|
||||
@ -88,10 +88,10 @@ class _BuildsChartState extends State<BuildsChart> {
|
||||
color: const Color(0xff0a7005),
|
||||
value: (widget.nrbuilds -
|
||||
widget.nrfailedbuilds -
|
||||
widget.nrActiveBuilds)
|
||||
widget.nrEnqueuedBuilds)
|
||||
.toDouble(),
|
||||
title:
|
||||
"${((widget.nrbuilds - widget.nrfailedbuilds - widget.nrActiveBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
||||
"${((widget.nrbuilds - widget.nrfailedbuilds - widget.nrEnqueuedBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
||||
radius: radius,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: fontSize,
|
||||
@ -101,9 +101,9 @@ class _BuildsChartState extends State<BuildsChart> {
|
||||
case 2:
|
||||
return PieChartSectionData(
|
||||
color: const Color(0xFF0044AA),
|
||||
value: (widget.nrActiveBuilds).toDouble(),
|
||||
value: (widget.nrEnqueuedBuilds).toDouble(),
|
||||
title:
|
||||
"${((widget.nrActiveBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
||||
"${((widget.nrEnqueuedBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
||||
radius: radius,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: fontSize,
|
||||
|
@ -5,17 +5,11 @@ import 'package:aurcache/providers/api/builds_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../constants/color_constants.dart';
|
||||
import '../table_info.dart';
|
||||
|
||||
class RecentBuilds extends StatefulWidget {
|
||||
const RecentBuilds({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
class RecentBuilds extends StatelessWidget {
|
||||
const RecentBuilds({super.key});
|
||||
|
||||
@override
|
||||
State<RecentBuilds> createState() => _RecentBuildsState();
|
||||
}
|
||||
|
||||
class _RecentBuildsState extends State<RecentBuilds> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -31,18 +25,20 @@ class _RecentBuildsState extends State<RecentBuilds> {
|
||||
"Recent Builds",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: APIBuilder<BuildsProvider, List<Build>, BuildsDTO>(
|
||||
APIBuilder<BuildsProvider, List<Build>, BuildsDTO>(
|
||||
key: const Key("Builds on dashboard"),
|
||||
dto: BuildsDTO(limit: 10),
|
||||
interval: const Duration(seconds: 10),
|
||||
onLoad: () => const Text("no data"),
|
||||
onData: (t) {
|
||||
return BuildsTable(data: t);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (t.isEmpty) {
|
||||
return const TableInfo(title: "You have no builds yet");
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity, child: BuildsTable(data: t)),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.push("/builds");
|
||||
@ -51,7 +47,12 @@ class _RecentBuildsState extends State<RecentBuilds> {
|
||||
"List all Builds",
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -35,10 +35,34 @@ class SidePanel extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: defaultPadding),
|
||||
BuildsChart(
|
||||
nrbuilds > 0
|
||||
? BuildsChart(
|
||||
nrbuilds: nrbuilds,
|
||||
nrfailedbuilds: nrfailedbuilds,
|
||||
nrActiveBuilds: nrEnqueuedBuilds),
|
||||
nrEnqueuedBuilds: nrEnqueuedBuilds)
|
||||
: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 42,
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Text("Add Packages to view Graph"),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SideCard(
|
||||
color: const Color(0xff0a7005),
|
||||
title: "Successful Builds",
|
||||
|
@ -5,17 +5,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../constants/color_constants.dart';
|
||||
import '../../models/package.dart';
|
||||
import '../table_info.dart';
|
||||
|
||||
class YourPackages extends StatefulWidget {
|
||||
const YourPackages({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
class YourPackages extends StatelessWidget {
|
||||
const YourPackages({super.key});
|
||||
|
||||
@override
|
||||
State<YourPackages> createState() => _YourPackagesState();
|
||||
}
|
||||
|
||||
class _YourPackagesState extends State<YourPackages> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -31,19 +25,20 @@ class _YourPackagesState extends State<YourPackages> {
|
||||
"Your Packages",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: APIBuilder<PackagesProvider, List<Package>, PackagesDTO>(
|
||||
APIBuilder<PackagesProvider, List<Package>, PackagesDTO>(
|
||||
key: const Key("Packages on dashboard"),
|
||||
interval: const Duration(seconds: 10),
|
||||
dto: PackagesDTO(limit: 10),
|
||||
onData: (data) {
|
||||
return PackagesTable(data: data);
|
||||
},
|
||||
onLoad: () => const Text("No data"),
|
||||
),
|
||||
),
|
||||
if (data.isEmpty) {
|
||||
return const TableInfo(title: "You have no packages yet");
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: PackagesTable(data: data)),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.push("/packages");
|
||||
@ -53,39 +48,14 @@ class _YourPackagesState extends State<YourPackages> {
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
)
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
onLoad: () => const CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IconData switchSuccessIcon(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return Icons.watch_later_outlined;
|
||||
case 1:
|
||||
return Icons.check_circle_outline;
|
||||
case 2:
|
||||
return Icons.cancel_outlined;
|
||||
case 3:
|
||||
return Icons.pause_circle_outline;
|
||||
default:
|
||||
return Icons.question_mark_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
Color switchSuccessColor(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return const Color(0xFF9D8D00);
|
||||
case 1:
|
||||
return const Color(0xFF0A6900);
|
||||
case 2:
|
||||
return const Color(0xff760707);
|
||||
case 3:
|
||||
return const Color(0xFF0044AA);
|
||||
default:
|
||||
return const Color(0xFF9D8D00);
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import '../models/package.dart';
|
||||
import '../providers/api/builds_provider.dart';
|
||||
import '../providers/api/packages_provider.dart';
|
||||
import '../providers/api/stats_provider.dart';
|
||||
import '../utils/package_color.dart';
|
||||
import 'confirm_popup.dart';
|
||||
import 'dashboard/your_packages.dart';
|
||||
|
||||
class PackagesTable extends StatelessWidget {
|
||||
const PackagesTable({super.key, required this.data});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../utils/responsive.dart';
|
||||
import '../../screens/dashboard_screen.dart';
|
||||
import 'side_menu.dart';
|
||||
|
||||
class MenuShell extends StatelessWidget {
|
||||
|
@ -6,8 +6,8 @@ import '../../constants/color_constants.dart';
|
||||
|
||||
class SideMenu extends StatelessWidget {
|
||||
const SideMenu({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -20,13 +20,10 @@ class SideMenu extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// SizedBox(
|
||||
// height: defaultPadding * 3,
|
||||
// ),
|
||||
// Image.asset(
|
||||
// "assets/logo/logo_icon.png",
|
||||
// scale: 5,
|
||||
// ),
|
||||
SizedBox(
|
||||
height: defaultPadding,
|
||||
),
|
||||
Icon(Icons.storage_sharp, size: 60, color: Colors.white54),
|
||||
SizedBox(
|
||||
height: defaultPadding,
|
||||
),
|
||||
@ -68,12 +65,11 @@ class SideMenu extends StatelessWidget {
|
||||
|
||||
class DrawerListTile extends StatelessWidget {
|
||||
const DrawerListTile({
|
||||
Key? key,
|
||||
// For selecting those three line once press "Command+D"
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.svgSrc,
|
||||
required this.press,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final String title, svgSrc;
|
||||
final VoidCallback press;
|
||||
|
30
frontend/lib/components/table_info.dart
Normal file
30
frontend/lib/components/table_info.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TableInfo extends StatelessWidget {
|
||||
const TableInfo({super.key, required this.title});
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
const Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 42,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Text(title),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -36,7 +36,9 @@ class _AurScreenState extends State<AurScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
title: const Text("AUR"),
|
||||
),
|
||||
body: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => PackagesProvider()),
|
||||
|
@ -12,9 +12,9 @@ import 'package:provider/provider.dart';
|
||||
import '../api/API.dart';
|
||||
import '../components/confirm_popup.dart';
|
||||
import '../components/dashboard/chart_card.dart';
|
||||
import '../components/dashboard/your_packages.dart';
|
||||
import '../constants/color_constants.dart';
|
||||
import '../providers/api/build_provider.dart';
|
||||
import '../utils/package_color.dart';
|
||||
|
||||
class BuildScreen extends StatefulWidget {
|
||||
const BuildScreen({super.key, required this.buildID});
|
||||
@ -184,37 +184,7 @@ class _BuildScreenState extends State<BuildScreen> {
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final confirmResult = await showConfirmationDialog(
|
||||
context,
|
||||
"Delete Build",
|
||||
"Are you sure to delete this Package?", () {
|
||||
API.deleteBuild(widget.buildID);
|
||||
context.pop();
|
||||
}, null);
|
||||
},
|
||||
child: const Text(
|
||||
"Delete",
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final buildid =
|
||||
await API.updatePackage(id: buildData.pkg_id);
|
||||
context.pushReplacement("/build/$buildid");
|
||||
},
|
||||
child: const Text(
|
||||
"Retry",
|
||||
style: TextStyle(color: Colors.orangeAccent),
|
||||
),
|
||||
),
|
||||
],
|
||||
children: buildActions(buildData),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
@ -233,7 +203,7 @@ class _BuildScreenState extends State<BuildScreen> {
|
||||
),
|
||||
SideCard(
|
||||
title: "Build Number",
|
||||
textRight: buildData.id.toString(),
|
||||
textRight: "#${buildData.id}",
|
||||
),
|
||||
SideCard(
|
||||
title: "Version",
|
||||
@ -257,6 +227,58 @@ class _BuildScreenState extends State<BuildScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildActions(Build build) {
|
||||
if (build.status == 0) {
|
||||
return [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await showConfirmationDialog(
|
||||
context, "Cancel Build", "Are you sure to cancel this Build?",
|
||||
() {
|
||||
API.cancelBuild(widget.buildID);
|
||||
Provider.of<BuildProvider>(context, listen: false)
|
||||
.refresh(context);
|
||||
}, null);
|
||||
},
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await showConfirmationDialog(
|
||||
context, "Delete Build", "Are you sure to delete this Build?",
|
||||
() {
|
||||
API.deleteBuild(widget.buildID);
|
||||
context.pop();
|
||||
}, null);
|
||||
},
|
||||
child: const Text(
|
||||
"Delete",
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final buildid = await API.updatePackage(id: build.pkg_id);
|
||||
context.pushReplacement("/build/$buildid");
|
||||
},
|
||||
child: const Text(
|
||||
"Retry",
|
||||
style: TextStyle(color: Colors.orangeAccent),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPage(Build build) {
|
||||
switch (build.status) {
|
||||
case 3:
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:aurcache/components/builds_table.dart';
|
||||
import 'package:aurcache/components/api/APIBuilder.dart';
|
||||
import 'package:aurcache/components/table_info.dart';
|
||||
import 'package:aurcache/providers/api/builds_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -12,7 +13,9 @@ class BuildsScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
title: const Text("All Builds"),
|
||||
),
|
||||
body: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<BuildsProvider>(
|
||||
@ -41,7 +44,12 @@ class BuildsScreen extends StatelessWidget {
|
||||
interval: const Duration(seconds: 10),
|
||||
onLoad: () => const Text("no data"),
|
||||
onData: (data) {
|
||||
if (data.isEmpty) {
|
||||
return const TableInfo(
|
||||
title: "You have no builds yet");
|
||||
} else {
|
||||
return BuildsTable(data: data);
|
||||
}
|
||||
}),
|
||||
)
|
||||
],
|
||||
|
@ -60,7 +60,6 @@ class _PackageScreenState extends State<PackageScreen> {
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final confirmResult =
|
||||
await showConfirmationDialog(
|
||||
context,
|
||||
"Force update Package",
|
||||
@ -91,7 +90,6 @@ class _PackageScreenState extends State<PackageScreen> {
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final confirmResult =
|
||||
await showConfirmationDialog(
|
||||
context,
|
||||
"Delete Package",
|
||||
@ -121,7 +119,7 @@ class _PackageScreenState extends State<PackageScreen> {
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 15,
|
||||
)
|
||||
],
|
||||
|
34
frontend/lib/utils/package_color.dart
Normal file
34
frontend/lib/utils/package_color.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
IconData switchSuccessIcon(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return Icons.watch_later_outlined;
|
||||
case 1:
|
||||
return Icons.check_circle_outline;
|
||||
case 2:
|
||||
return Icons.cancel_outlined;
|
||||
case 3:
|
||||
return Icons.pause_circle_outline;
|
||||
case 4:
|
||||
return Icons.remove_circle_outline;
|
||||
default:
|
||||
return Icons.question_mark_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
Color switchSuccessColor(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return const Color(0xFF9D8D00);
|
||||
case 1:
|
||||
return const Color(0xFF0A6900);
|
||||
case 4:
|
||||
case 2:
|
||||
return const Color(0xff760707);
|
||||
case 3:
|
||||
return const Color(0xFF0044AA);
|
||||
default:
|
||||
return const Color(0xFF9D8D00);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ extension TimeFormatter on DateTime {
|
||||
if (duration.inSeconds < 60) {
|
||||
return '${duration.inSeconds} Second${_s(duration.inSeconds)} ago';
|
||||
} else if (duration.inMinutes < 60) {
|
||||
return '${duration.inMinutes} Minute${_s(duration.inMinutes)})} ago';
|
||||
return '${duration.inMinutes} Minute${_s(duration.inMinutes)} ago';
|
||||
} else if (duration.inHours < 24) {
|
||||
return '${duration.inHours} Hour${_s(duration.inHours)} ago';
|
||||
} else if (duration.inDays < 30) {
|
||||
|
BIN
res/imgs/screenshot1.png
Normal file
BIN
res/imgs/screenshot1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 311 KiB |
BIN
res/imgs/screenshot2.png
Normal file
BIN
res/imgs/screenshot2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 404 KiB |
BIN
res/imgs/screenshot3.png
Normal file
BIN
res/imgs/screenshot3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 KiB |
Loading…
Reference in New Issue
Block a user