mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 15:49:00 +02:00
Call to action (switch to Next theme, enable background effects) (#6625)
This commit is contained in:
commit
c2e9f4764b
@ -30,6 +30,7 @@ import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolb
|
|||||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import NoteListWidget from "../widgets/note_list.js";
|
import NoteListWidget from "../widgets/note_list.js";
|
||||||
|
import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx";
|
||||||
|
|
||||||
export function applyModals(rootContainer: RootContainer) {
|
export function applyModals(rootContainer: RootContainer) {
|
||||||
rootContainer
|
rootContainer
|
||||||
@ -66,4 +67,5 @@ export function applyModals(rootContainer: RootContainer) {
|
|||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(new NoteListWidget(true)))
|
.child(new NoteListWidget(true)))
|
||||||
|
.child(new CallToActionDialog());
|
||||||
}
|
}
|
||||||
|
@ -1992,5 +1992,13 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"help_title": "Display more information about this screen"
|
"help_title": "Display more information about this screen"
|
||||||
|
},
|
||||||
|
"call_to_action": {
|
||||||
|
"next_theme_title": "TriliumNext theme is now stable",
|
||||||
|
"next_theme_message": "For a while now, we've been working on a new theme to give the application a more modern look.",
|
||||||
|
"next_theme_button": "Switch to the TriliumNext theme",
|
||||||
|
"background_effects_title": "Background effects are now stable",
|
||||||
|
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.",
|
||||||
|
"background_effects_button": "Enable background effects"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
58
apps/client/src/widgets/dialogs/call_to_action.tsx
Normal file
58
apps/client/src/widgets/dialogs/call_to_action.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import Button from "../react/Button";
|
||||||
|
import Modal from "../react/Modal";
|
||||||
|
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||||
|
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||||
|
|
||||||
|
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
|
||||||
|
if (!activeCallToActions.length) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||||
|
const [ shown, setShown ] = useState(true);
|
||||||
|
const activeItem = activeCallToActions[activeIndex];
|
||||||
|
|
||||||
|
function goToNext() {
|
||||||
|
if (activeIndex + 1 < activeCallToActions.length) {
|
||||||
|
setActiveIndex(activeIndex + 1);
|
||||||
|
} else {
|
||||||
|
setShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="call-to-action"
|
||||||
|
size="md"
|
||||||
|
title="New features"
|
||||||
|
show={shown}
|
||||||
|
onHidden={() => setShown(false)}
|
||||||
|
footerAlignment="between"
|
||||||
|
footer={<>
|
||||||
|
<Button text="Dismiss" onClick={async () => {
|
||||||
|
await dismissCallToAction(activeItem.id);
|
||||||
|
goToNext();
|
||||||
|
}} />
|
||||||
|
{activeItem.buttons.map((button) =>
|
||||||
|
<Button text={button.text} onClick={async () => {
|
||||||
|
await dismissCallToAction(activeItem.id);
|
||||||
|
await button.onClick();
|
||||||
|
goToNext();
|
||||||
|
}}/>
|
||||||
|
)}
|
||||||
|
</>}
|
||||||
|
>
|
||||||
|
<h4>{activeItem.title}</h4>
|
||||||
|
<p>{activeItem.message}</p>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CallToActionDialog extends ReactBasicWidget {
|
||||||
|
|
||||||
|
get component() {
|
||||||
|
return <CallToActionDialogComponent activeCallToActions={getCallToActions()} />
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
116
apps/client/src/widgets/dialogs/call_to_action_definitions.ts
Normal file
116
apps/client/src/widgets/dialogs/call_to_action_definitions.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import utils from "../../services/utils";
|
||||||
|
import options from "../../services/options";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "call-to-action" is an interactive message for the user, generally to present new features.
|
||||||
|
*/
|
||||||
|
export interface CallToAction {
|
||||||
|
/**
|
||||||
|
* A unique identifier to allow the call-to-action to be dismissed by the user and then never shown again.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The title of the call-to-action, displayed as a heading in the message.
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The message body of the call-to-action.
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
/**
|
||||||
|
* Function that determines whether the call-to-action can be displayed to the user. The check can be based on options or
|
||||||
|
* the platform of the user. If the user already dismissed this call-to-action, the value of this function doesn't matter.
|
||||||
|
*
|
||||||
|
* @returns whether to allow this call-to-action or to skip it, based on the user's environment.
|
||||||
|
*/
|
||||||
|
enabled: () => boolean;
|
||||||
|
/**
|
||||||
|
* A list of buttons to display in the footer of the modal.
|
||||||
|
*/
|
||||||
|
buttons: {
|
||||||
|
/**
|
||||||
|
* The text displayed on the button.
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
/**
|
||||||
|
* The listener that will be called when the button is pressed. Async methods are supported and will be awaited before proceeding to the next step.
|
||||||
|
*/
|
||||||
|
onClick: () => (void | Promise<void>);
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNextTheme() {
|
||||||
|
return [ "next", "next-light", "next-dark" ].includes(options.get("theme"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const CALL_TO_ACTIONS: CallToAction[] = [
|
||||||
|
{
|
||||||
|
id: "next_theme",
|
||||||
|
title: t("call_to_action.next_theme_title"),
|
||||||
|
message: t("call_to_action.next_theme_message"),
|
||||||
|
enabled: () => !isNextTheme(),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: t("call_to_action.next_theme_button"),
|
||||||
|
async onClick() {
|
||||||
|
await options.save("theme", "next");
|
||||||
|
await options.save("backgroundEffects", "true");
|
||||||
|
utils.reloadFrontendApp("call-to-action");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "background_effects",
|
||||||
|
title: t("call_to_action.background_effects_title"),
|
||||||
|
message: t("call_to_action.background_effects_message"),
|
||||||
|
enabled: () => utils.isElectron() && window.glob.platform === "win32" && isNextTheme() && !options.is("backgroundEffects"),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: t("call_to_action.background_effects_button"),
|
||||||
|
async onClick() {
|
||||||
|
await options.save("backgroundEffects", "true");
|
||||||
|
utils.restartDesktopApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the list of available call-to-actions, meaning those that are enabled based on the user's environment but also with
|
||||||
|
* without the call-to-actions already dismissed by the user.
|
||||||
|
*
|
||||||
|
* @returns a list iof call to actions to display to the user.
|
||||||
|
*/
|
||||||
|
export function getCallToActions() {
|
||||||
|
const seenCallToActions = new Set(getSeenCallToActions());
|
||||||
|
|
||||||
|
return CALL_TO_ACTIONS.filter((callToAction) =>
|
||||||
|
!seenCallToActions.has(callToAction.id) && callToAction.enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the call-to-action as dismissed by the user, meaning that {@link getCallToActions()} will no longer list this particular action.
|
||||||
|
*
|
||||||
|
* @param id the corresponding ID of the call to action.
|
||||||
|
* @returns a promise with the option saving. Generally it's best to wait for the promise to resolve before refreshing the page.
|
||||||
|
*/
|
||||||
|
export async function dismissCallToAction(id: string) {
|
||||||
|
const seenCallToActions = getSeenCallToActions();
|
||||||
|
if (seenCallToActions.find(seenId => seenId === id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenCallToActions.push(id);
|
||||||
|
await options.save("seenCallToActions", JSON.stringify(seenCallToActions));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeenCallToActions() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(options.get("seenCallToActions")) as string[];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
@ -93,6 +93,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
|||||||
"redirectBareDomain",
|
"redirectBareDomain",
|
||||||
"showLoginInShareTheme",
|
"showLoginInShareTheme",
|
||||||
"splitEditorOrientation",
|
"splitEditorOrientation",
|
||||||
|
"seenCallToActions",
|
||||||
|
|
||||||
// AI/LLM integration options
|
// AI/LLM integration options
|
||||||
"aiEnabled",
|
"aiEnabled",
|
||||||
|
@ -183,7 +183,7 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
|
|
||||||
// HTML import configuration
|
// HTML import configuration
|
||||||
{ name: "layoutOrientation", value: "vertical", isSynced: false },
|
{ name: "layoutOrientation", value: "vertical", isSynced: false },
|
||||||
{ name: "backgroundEffects", value: "false", isSynced: false },
|
{ name: "backgroundEffects", value: "true", isSynced: false },
|
||||||
{
|
{
|
||||||
name: "allowedHtmlTags",
|
name: "allowedHtmlTags",
|
||||||
value: JSON.stringify(DEFAULT_ALLOWED_TAGS),
|
value: JSON.stringify(DEFAULT_ALLOWED_TAGS),
|
||||||
@ -206,11 +206,11 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
{ name: "ollamaEnabled", value: "false", isSynced: true },
|
{ name: "ollamaEnabled", value: "false", isSynced: true },
|
||||||
{ name: "ollamaDefaultModel", value: "", isSynced: true },
|
{ name: "ollamaDefaultModel", value: "", isSynced: true },
|
||||||
{ name: "ollamaBaseUrl", value: "http://localhost:11434", isSynced: true },
|
{ name: "ollamaBaseUrl", value: "http://localhost:11434", isSynced: true },
|
||||||
|
|
||||||
// Adding missing AI options
|
|
||||||
{ name: "aiTemperature", value: "0.7", isSynced: true },
|
{ name: "aiTemperature", value: "0.7", isSynced: true },
|
||||||
{ name: "aiSystemPrompt", value: "", isSynced: true },
|
{ name: "aiSystemPrompt", value: "", isSynced: true },
|
||||||
{ name: "aiSelectedProvider", value: "openai", isSynced: true },
|
{ name: "aiSelectedProvider", value: "openai", isSynced: true },
|
||||||
|
|
||||||
|
{ name: "seenCallToActions", value: "[]", isSynced: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,7 +145,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
|||||||
ollamaDefaultModel: string;
|
ollamaDefaultModel: string;
|
||||||
codeOpenAiModel: string;
|
codeOpenAiModel: string;
|
||||||
aiSelectedProvider: string;
|
aiSelectedProvider: string;
|
||||||
|
seenCallToActions: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OptionNames = keyof OptionDefinitions;
|
export type OptionNames = keyof OptionDefinitions;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user