You've already forked decomp.dev
mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-03-30 11:06:20 -07:00
Add platform selection on projects page
This commit is contained in:
+25
-30
@@ -156,57 +156,53 @@ pub struct FrogressMapping {
|
||||
pub project_measure: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Platform {
|
||||
GBA,
|
||||
GBC,
|
||||
N64,
|
||||
DS,
|
||||
PS,
|
||||
PS2,
|
||||
Switch,
|
||||
GC,
|
||||
Wii,
|
||||
PS, // 1994
|
||||
N64, // 1996
|
||||
PS2, // 2000
|
||||
GBA, // 2001
|
||||
GC, // 2001
|
||||
DS, // 2004
|
||||
Wii, // 2006
|
||||
Switch, // 2017
|
||||
}
|
||||
|
||||
pub const ALL_PLATFORMS: &[Platform] = &[
|
||||
Platform::GBA,
|
||||
Platform::GBC,
|
||||
Platform::N64,
|
||||
Platform::DS,
|
||||
Platform::PS,
|
||||
Platform::N64,
|
||||
Platform::PS2,
|
||||
Platform::Switch,
|
||||
Platform::GBA,
|
||||
Platform::GC,
|
||||
Platform::DS,
|
||||
Platform::Wii,
|
||||
Platform::Switch,
|
||||
];
|
||||
|
||||
impl Platform {
|
||||
pub fn to_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::GBA => "gba",
|
||||
Self::GBC => "gbc",
|
||||
Self::N64 => "n64",
|
||||
Self::DS => "nds",
|
||||
Self::PS => "ps",
|
||||
Self::N64 => "n64",
|
||||
Self::PS2 => "ps2",
|
||||
Self::Switch => "switch",
|
||||
Self::GBA => "gba",
|
||||
Self::GC => "gc",
|
||||
Self::DS => "nds",
|
||||
Self::Wii => "wii",
|
||||
Self::Switch => "switch",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Platform::GBA => "Game Boy Advance",
|
||||
Platform::GBC => "Game Boy Color",
|
||||
Platform::N64 => "Nintendo 64",
|
||||
Platform::DS => "Nintendo DS",
|
||||
Platform::PS => "PlayStation",
|
||||
Platform::N64 => "Nintendo 64",
|
||||
Platform::PS2 => "PlayStation 2",
|
||||
Platform::Switch => "Nintendo Switch",
|
||||
Platform::GBA => "Game Boy Advance",
|
||||
Platform::GC => "GameCube",
|
||||
Platform::DS => "Nintendo DS",
|
||||
Platform::Wii => "Wii",
|
||||
Platform::Switch => "Switch",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,15 +212,14 @@ impl FromStr for Platform {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"gba" => Ok(Self::GBA),
|
||||
"gbc" => Ok(Self::GBC),
|
||||
"n64" => Ok(Self::N64),
|
||||
"nds" => Ok(Self::DS),
|
||||
"ps" => Ok(Self::PS),
|
||||
"n64" => Ok(Self::N64),
|
||||
"ps2" => Ok(Self::PS2),
|
||||
"switch" => Ok(Self::Switch),
|
||||
"gba" => Ok(Self::GBA),
|
||||
"gc" => Ok(Self::GC),
|
||||
"nds" => Ok(Self::DS),
|
||||
"wii" => Ok(Self::Wii),
|
||||
"switch" => Ok(Self::Switch),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ fn generate_changes_list(changes: Vec<ChangeLine>, out: &mut String) {
|
||||
} else {
|
||||
out.push_str("<details>\n");
|
||||
}
|
||||
out.push_str(&format!("<summary>{emoji} {total_changes} {description}:</summary>\n"));
|
||||
out.push_str(&format!("<summary>{emoji} {total_changes} {description}</summary>\n"));
|
||||
out.push('\n'); // Must include a blank line before a table
|
||||
out.push_str("| Unit | Function | Bytes | Before | After |\n");
|
||||
out.push_str("| - | - | - | - | - |\n");
|
||||
|
||||
@@ -11,11 +11,12 @@ use decomp_dev_auth::CurrentUser;
|
||||
use decomp_dev_core::{
|
||||
AppError, FullUri,
|
||||
models::{
|
||||
CachedReportFile, Commit, Platform, ProjectInfo, ProjectVisibility, ReportInner,
|
||||
project_visibility,
|
||||
ALL_PLATFORMS, CachedReportFile, Commit, Platform, ProjectInfo, ProjectVisibility,
|
||||
ReportInner, project_visibility,
|
||||
},
|
||||
util::{UrlExt, format_percent, size},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use maud::{DOCTYPE, Markup, html};
|
||||
use objdiff_core::bindings::report::Measures;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -42,6 +43,7 @@ struct ProjectInfoContext {
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProjectsQuery {
|
||||
sort: Option<String>,
|
||||
platform: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Copy, Clone)]
|
||||
@@ -157,7 +159,36 @@ pub async fn get_projects(
|
||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||
}
|
||||
|
||||
let projects = state.db.get_projects().await?;
|
||||
let platforms = query
|
||||
.platform
|
||||
.as_deref()
|
||||
.into_iter()
|
||||
.flat_map(|s| s.split(','))
|
||||
.filter_map(|s| Platform::from_str(s).ok())
|
||||
.sorted()
|
||||
.dedup()
|
||||
.collect::<Vec<_>>();
|
||||
let show_all = platforms.is_empty() || platforms == ALL_PLATFORMS;
|
||||
|
||||
let mut projects = state.db.get_projects().await?;
|
||||
|
||||
let available_platforms = projects
|
||||
.iter()
|
||||
.filter_map(|p| p.project.platform.as_deref())
|
||||
.filter_map(|s| Platform::from_str(s).ok())
|
||||
.sorted()
|
||||
.dedup_with_count()
|
||||
.collect::<Vec<_>>();
|
||||
if !show_all {
|
||||
projects.retain(|p| {
|
||||
p.project
|
||||
.platform
|
||||
.as_deref()
|
||||
.and_then(|p| Platform::from_str(p).ok())
|
||||
.is_some_and(|p| platforms.contains(&p))
|
||||
});
|
||||
}
|
||||
|
||||
let mut out = projects
|
||||
.iter()
|
||||
.map(|p| ProjectInfoContext {
|
||||
@@ -261,7 +292,17 @@ pub async fn get_projects(
|
||||
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||
{
|
||||
return render_project(ctx, out, uri, current_sort, current_user.as_ref()).await;
|
||||
return render_project(
|
||||
ctx,
|
||||
out,
|
||||
uri,
|
||||
current_sort,
|
||||
current_user.as_ref(),
|
||||
&available_platforms,
|
||||
&platforms,
|
||||
show_all,
|
||||
)
|
||||
.await;
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||
let projects = out
|
||||
.into_iter()
|
||||
@@ -285,6 +326,9 @@ async fn render_project(
|
||||
uri: Uri,
|
||||
current_sort: SortOption,
|
||||
current_user: Option<&CurrentUser>,
|
||||
available_platforms: &[(usize, Platform)],
|
||||
platforms: &[Platform],
|
||||
show_all: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
let mut combined_styles = ProgressSections { nonce: ctx.nonce.clone(), ..Default::default() };
|
||||
for info in &mut out {
|
||||
@@ -293,6 +337,7 @@ async fn render_project(
|
||||
|
||||
let request_url = Url::parse(&uri.to_string()).context("Failed to parse URI")?;
|
||||
let canonical_url = request_url.with_path("/projects");
|
||||
let is_primary_view = show_all && current_sort.key == "updated";
|
||||
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
@@ -302,12 +347,17 @@ async fn render_project(
|
||||
title { "Projects • decomp.dev" }
|
||||
(ctx.header().await)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
(ctx.chunks("projects", Load::Deferred).await)
|
||||
link rel="canonical" href=(canonical_url);
|
||||
meta name="description" content="Decompilation progress reports";
|
||||
meta property="og:title" content="Decompilation progress reports";
|
||||
meta property="og:description" content="Progress reports for matching decompilation projects";
|
||||
meta property="og:image" content=(canonical_url.with_path("/og.png"));
|
||||
meta property="og:url" content=(canonical_url);
|
||||
@if !is_primary_view {
|
||||
// Prevent search engines from indexing anything but the primary view
|
||||
meta name="robots" content="noindex";
|
||||
}
|
||||
}
|
||||
body {
|
||||
header {
|
||||
@@ -319,20 +369,6 @@ async fn render_project(
|
||||
li {
|
||||
a href="/projects" { "Projects" }
|
||||
}
|
||||
li.md {
|
||||
details.dropdown {
|
||||
summary { (current_sort.name) }
|
||||
ul {
|
||||
@for option in SORT_OPTIONS {
|
||||
li {
|
||||
a href=(request_url.query_param("sort", Some(option.key))) {
|
||||
(option.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(nav_links())
|
||||
}
|
||||
@@ -349,13 +385,45 @@ async fn render_project(
|
||||
}
|
||||
}
|
||||
main {
|
||||
details.dropdown.sm {
|
||||
summary { (current_sort.name) }
|
||||
ul {
|
||||
@for option in SORT_OPTIONS {
|
||||
.grid.platform-grid {
|
||||
details.dropdown.platform-dropdown {
|
||||
summary {
|
||||
@if show_all {
|
||||
"All Platforms"
|
||||
} @else if platforms.len() == 1 {
|
||||
(platforms[0].name())
|
||||
} @else {
|
||||
(platforms.len()) " Platforms"
|
||||
}
|
||||
}
|
||||
ul {
|
||||
li {
|
||||
a href=(request_url.query_param("sort", Some(option.key))) {
|
||||
(option.name)
|
||||
a href=(request_url.query_param("platform", None)) {
|
||||
"All Platforms"
|
||||
}
|
||||
}
|
||||
@for (n, platform) in available_platforms {
|
||||
li.platform-item {
|
||||
label {
|
||||
input type="checkbox" name="platform" value=(platform.to_str())
|
||||
checked[show_all || platforms.contains(platform)];
|
||||
span.platform-icon.(format!("icon-{}", platform.to_str())) {}
|
||||
(platform.name())
|
||||
span.count-badge { (n) }
|
||||
}
|
||||
button.secondary { "Only" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
details.dropdown {
|
||||
summary { (current_sort.name) }
|
||||
ul {
|
||||
@for option in SORT_OPTIONS {
|
||||
li {
|
||||
a href=(request_url.query_param("sort", Some(option.key))) {
|
||||
(option.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,13 +450,14 @@ fn project_fragment(
|
||||
let Some(commit) = ctx.report.as_ref().map(|r| r.commit.clone()) else {
|
||||
return Markup::default();
|
||||
};
|
||||
let project_path = canonical_url.with_path(&format!("/{}/{}", project.owner, project.repo));
|
||||
let mut project_path = canonical_url.with_path(&format!("/{}/{}", project.owner, project.repo));
|
||||
project_path.set_query(None);
|
||||
let commit_url =
|
||||
format!("https://github.com/{}/{}/commit/{}", project.owner, project.repo, commit.sha);
|
||||
let header_image_id = project.header_image_id.map(hex::encode);
|
||||
const HEADER_QUERY: &str = "?w=1024&h=256";
|
||||
html! {
|
||||
article.project {
|
||||
article.project data-platform=[project.platform.as_deref()] {
|
||||
a.project-link href=(project_path) aria-label="View project" {}
|
||||
@if let Some(header_image_id) = header_image_id {
|
||||
.project-image-container {
|
||||
|
||||
@@ -262,7 +262,6 @@ pub async fn get_report(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn mode_overview(
|
||||
scope: &Scope<'_>,
|
||||
state: &AppState,
|
||||
@@ -301,7 +300,6 @@ async fn mode_overview(
|
||||
Err(AppError::Status(StatusCode::NOT_ACCEPTABLE))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn mode_report(
|
||||
scope: &Scope<'_>,
|
||||
state: &AppState,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
mod cron;
|
||||
mod frogress;
|
||||
mod handlers;
|
||||
|
||||
+47
-1
@@ -264,7 +264,8 @@ $progress-height: 2rem;
|
||||
background-color: $jade-500;
|
||||
}
|
||||
|
||||
.progress-section:nth-of-type(3) {
|
||||
.progress-section:nth-of-type(3),
|
||||
.progress-section.fuzzy {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -548,3 +549,48 @@ details.dropdown {
|
||||
label:has(> input[type="checkbox"]) {
|
||||
margin-bottom: var(--pico-spacing);
|
||||
}
|
||||
|
||||
.platform-grid {
|
||||
margin-top: calc(var(--pico-block-spacing-vertical) * -2);
|
||||
}
|
||||
|
||||
.platform-dropdown {
|
||||
margin: 0;
|
||||
|
||||
li.platform-item {
|
||||
display: flex;
|
||||
padding: 0 !important;
|
||||
flex: 1;
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin: 0;
|
||||
padding-inline-start: var(--pico-spacing);
|
||||
|
||||
.platform-icon {
|
||||
color: var(--pico-form-element-color);
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.25em 0.5em;
|
||||
margin: 0.25em;
|
||||
margin-inline-end: var(--pico-spacing);
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 0.8em;
|
||||
background: var(--pico-code-kbd-background-color);
|
||||
color: var(--pico-code-kbd-color);
|
||||
border-radius: 1em;
|
||||
min-width: 1.5em;
|
||||
height: 1.5em;
|
||||
padding: 0 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
const platforms: Map<string, HTMLInputElement> = new Map();
|
||||
|
||||
function updateProjectVisibility() {
|
||||
const selectedPlatforms: string[] = [];
|
||||
let allSelected = true;
|
||||
for (const [platform, checkbox] of platforms.entries()) {
|
||||
if (checkbox.checked) {
|
||||
selectedPlatforms.push(platform);
|
||||
} else {
|
||||
allSelected = false;
|
||||
}
|
||||
}
|
||||
if (selectedPlatforms.length === 0) {
|
||||
allSelected = true;
|
||||
for (const cb of platforms.values()) {
|
||||
cb.checked = true;
|
||||
}
|
||||
}
|
||||
const url = new URL(window.location.href);
|
||||
if (allSelected) {
|
||||
if (url.searchParams.has('platform')) {
|
||||
url.searchParams.delete('platform');
|
||||
window.location.replace(url);
|
||||
}
|
||||
} else {
|
||||
url.searchParams.set('platform', selectedPlatforms.join(','));
|
||||
window.location.replace(url.toString().replace(/%2C/g, ','));
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.platform-item').forEach((item) => {
|
||||
const checkbox = item.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
|
||||
const button = item.querySelector('button') as HTMLButtonElement | null;
|
||||
if (!checkbox || !button) {
|
||||
return;
|
||||
}
|
||||
platforms.set(checkbox.value, checkbox);
|
||||
checkbox.addEventListener('click', (e) => e.stopPropagation());
|
||||
checkbox.addEventListener('change', () => updateProjectVisibility());
|
||||
button.addEventListener('click', () => {
|
||||
let allUnchecked = checkbox.checked;
|
||||
if (allUnchecked) {
|
||||
for (const cb of platforms.values()) {
|
||||
if (cb !== checkbox && cb.checked) {
|
||||
allUnchecked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allUnchecked) {
|
||||
for (const cb of platforms.values()) {
|
||||
cb.checked = true;
|
||||
}
|
||||
} else {
|
||||
// Enable this checkbox and disable all others
|
||||
checkbox.checked = true;
|
||||
for (const cb of platforms.values()) {
|
||||
if (cb !== checkbox) {
|
||||
cb.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateProjectVisibility();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ export default defineConfig({
|
||||
manage: ['./js/manage.ts'],
|
||||
report: ['./js/treemap.ts'],
|
||||
api: ['./js/api.tsx'],
|
||||
projects: ['./js/projects.ts'],
|
||||
},
|
||||
},
|
||||
output: {
|
||||
|
||||
Reference in New Issue
Block a user