persist ordering of apps as designed by user between boots

This commit is contained in:
dr-frmr 2024-09-22 00:56:49 -04:00
parent 1a20438376
commit 4317ac27e3
No known key found for this signature in database
6 changed files with 69 additions and 120 deletions

View File

@ -1,10 +1,9 @@
#![feature(let_chains)]
use crate::kinode::process::homepage::{AddRequest, Request as HomepageRequest};
use kinode_process_lib::{
await_message, call_init, get_blob, http, http::server, println, Address, LazyLoadBlob,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
/// Fetching OS version from main package
const CARGO_TOML: &str = include_str!("../../../../Cargo.toml");
@ -26,9 +25,11 @@ struct HomepageApp {
base64_icon: Option<String>,
widget: Option<String>,
order: u32,
favorite: bool,
favorite: bool, // **not currently used on frontend**
}
type PersistedAppOrder = HashMap<String, u32>;
wit_bindgen::generate!({
path: "target/wit",
world: "homepage-sys-v0",
@ -162,6 +163,11 @@ fn init(our: Address) {
.bind_http_path("/order", http_config)
.expect("failed to bind /order");
// load persisted app order
let mut persisted_app_order =
kinode_process_lib::get_typed_state(|bytes| serde_json::from_slice(bytes))
.unwrap_or(PersistedAppOrder::new());
loop {
let Ok(ref message) = await_message() else {
// we never send requests, so this will never happen
@ -238,11 +244,16 @@ fn init(our: Address) {
None,
);
};
for (app_id, order) in order_list {
if let Some(app) = app_data.get_mut(&app_id) {
app.order = order;
for (app_id, order) in &order_list {
if let Some(app) = app_data.get_mut(app_id) {
app.order = *order;
}
}
persisted_app_order = order_list.into_iter().collect();
println!("updated order of apps: {:?}", persisted_app_order);
kinode_process_lib::set_state(
&serde_json::to_vec(&persisted_app_order).unwrap(),
);
(server::HttpResponse::new(http::StatusCode::OK), None)
}
_ => (server::HttpResponse::new(http::StatusCode::NOT_FOUND), None),
@ -264,10 +275,11 @@ fn init(our: Address) {
path,
widget,
}) => {
let id = message.source().process.to_string();
app_data.insert(
message.source().process.to_string(),
id.clone(),
HomepageApp {
id: message.source().process.to_string(),
id: id.clone(),
process: message.source().process().to_string(),
package: message.source().package().to_string(),
publisher: message.source().publisher().to_string(),
@ -281,14 +293,20 @@ fn init(our: Address) {
label,
base64_icon: icon,
widget,
order: app_data.len() as u32,
order: if let Some(order) = persisted_app_order.get(&id) {
*order
} else {
app_data.len() as u32
},
favorite: DEFAULT_FAVES
.contains(&message.source().process.to_string().as_str()),
},
);
}
HomepageRequest::Remove => {
app_data.remove(&message.source().process.to_string());
let id = message.source().process.to_string();
app_data.remove(&id);
persisted_app_order.remove(&id);
}
HomepageRequest::SetStylesheet(new_stylesheet_string) => {
// ONLY settings:settings:sys may call this request

View File

@ -1,14 +1,13 @@
import React, { useState, useEffect, useMemo } from "react";
import useHomepageStore from "../store/homepageStore";
import usePersistentStore from "../store/persistentStore";
import useHomepageStore, { HomepageApp } from "../store/homepageStore";
import AppDisplay from "./AppDisplay";
const AllApps: React.FC = () => {
const { apps } = useHomepageStore();
const { appOrder, setAppOrder } = usePersistentStore();
const [expanded, setExpanded] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [visibleApps, setVisibleApps] = useState(5);
const [orderedApps, setOrderedApps] = useState<HomepageApp[]>([]);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
@ -26,13 +25,14 @@ const AllApps: React.FC = () => {
// Sort apps based on persisted order
const sortedApps = useMemo(() => {
const orderedApps = [...apps].sort((a, b) => {
return appOrder.indexOf(a.id) - appOrder.indexOf(b.id);
if (!orderedApps.length) {
setOrderedApps(apps);
}
const o = [...orderedApps].sort((a, b) => {
return a.order - b.order;
});
// Ensure all apps are included in the order
const missingApps = apps.filter((app) => !appOrder.includes(app.id));
return [...orderedApps, ...missingApps];
}, [apps, appOrder]);
return o;
}, [orderedApps, apps]);
const displayedApps = expanded
? sortedApps
@ -67,29 +67,42 @@ const AllApps: React.FC = () => {
const [movedApp] = newSortedApps.splice(dragIndex, 1);
newSortedApps.splice(dropIndex, 0, movedApp);
const newAppOrder = newSortedApps.map((app) => app.id);
setAppOrder(newAppOrder);
const updatedApps = newSortedApps.map((app, index) => ({
...app,
order: index
}));
setOrderedApps(updatedApps);
// Sync the order with the backend
fetch('/order', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(newSortedApps.map((app, index) => [app.id, index]))
});
handleDragEnd();
};
return (
<div id="all-apps" className={isMobile ? "mobile" : ""}>
<div
className={`apps-grid ${expanded ? "expanded" : ""} ${
isMobile ? "mobile" : ""
}`}
className={`apps-grid ${expanded ? "expanded" : ""} ${isMobile ? "mobile" : ""
}`}
>
{displayedApps.map((app, index) => (
<div
key={app.id}
key={`${app.id}-${app.order}`}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
onDrop={(e) => handleDrop(e, index)}
className={`app-wrapper ${
draggedIndex === index ? "dragging" : ""
} ${dragOverIndex === index ? "drag-over" : ""}`}
className={`app-wrapper ${draggedIndex === index ? "dragging" : ""
} ${dragOverIndex === index ? "drag-over" : ""}`}
>
<AppDisplay app={app} />
<div className="drag-handle"></div>

View File

@ -14,10 +14,10 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
style={
!app?.path
? {
pointerEvents: "none",
textDecoration: "none !important",
filter: "grayscale(100%)",
}
pointerEvents: "none",
textDecoration: "none !important",
filter: "grayscale(100%)",
}
: {}
}
>

View File

@ -1,82 +0,0 @@
import useHomepageStore, { HomepageApp } from "../store/homepageStore"
import usePersistentStore from "../store/persistentStore"
import AppDisplay from "./AppDisplay"
import { useEffect, useState } from "react"
import { DragDropContext, Draggable, DropResult, Droppable } from '@hello-pangea/dnd'
const AppsDock: React.FC = () => {
const { apps } = useHomepageStore()
const { appOrder, setAppOrder } = usePersistentStore()
const [dockedApps, setDockedApps] = useState<HomepageApp[]>([])
useEffect(() => {
// Sort apps based on persisted order
const orderedApps = apps.filter(app => app.favorite).sort((a, b) => {
return appOrder.indexOf(a.id) - appOrder.indexOf(b.id);
});
setDockedApps(orderedApps);
// Sync the order with the backend
fetch('/order', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(orderedApps.map(app => [app.id, appOrder.indexOf(app.id)]))
});
}, [apps, appOrder])
const onDragEnd = (result: DropResult) => {
if (!result.destination) {
return;
}
const reorderedApps = Array.from(dockedApps);
const [reorderedItem] = reorderedApps.splice(result.source.index, 1);
reorderedApps.splice(result.destination.index, 0, reorderedItem);
const newAppOrder = reorderedApps.map(app => app.id);
setAppOrder(newAppOrder);
setDockedApps(reorderedApps);
fetch('/order', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(reorderedApps.map((app, index) => [app.id, index]))
});
}
return <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" direction="horizontal">
{(provided, _snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{dockedApps.map((app, index) => <Draggable
key={app.id}
draggableId={app.id}
index={index}
>
{(provided, _snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="docked-app"
>
<AppDisplay app={app} />
</div>
)}
</Draggable>)}
</div>
)}
</Droppable>
</DragDropContext>
}
export default AppsDock

View File

@ -174,7 +174,7 @@ footer {
border-radius: 0 0 1em 1em;
border: 0.5px solid rgba(255, 255, 255, 0.2);
padding: 1em;
color: light-dark(var(--white), var(--tasteful-dark));
color: var(--white);
}
@media (max-width: 1024px) {

View File

@ -46,13 +46,13 @@ function Homepage() {
<KinodeBird />
<h2>
{new Date().getHours() < 4
? "Good evening"
? "Good evening" // midnight to 4am
: new Date().getHours() < 12
? "Good morning"
? "Good morning" // 4am to 11am
: new Date().getHours() < 18
? "Good afternoon"
: "Good evening"}
, {our}
? "Good afternoon" // 12pm to 5pm
: "Good evening" // 5pm to midnight
}, {our}
</h2>
<a
href="https://github.com/kinode-dao/kinode/releases"