mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-22 16:11:38 +03:00
persist ordering of apps as designed by user between boots
This commit is contained in:
parent
1a20438376
commit
4317ac27e3
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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%)",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
|
@ -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
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user