Merge branch acc into master

This commit is contained in:
crschnick 2023-09-27 00:47:51 +00:00
parent f295286a8b
commit 227bcb8015
330 changed files with 5406 additions and 71419 deletions

37
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,37 @@
# Contributors guide
If you're interested in contributing to XPipe, you can easily do so! Just submit a pull request with your changes.
In terms of development environment setup, be sure to read the [development page](https://github.com/xpipe-io/xpipe/blob/master/DEVELOPMENT.md) first.
Especially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement:
### Implementing support for a new editor
All code for handling external editors can be found [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java). There you will find plenty of working examples that you can use as a base for your own implementation.
### Implementing support for a new terminal
All code for handling external terminals can be found [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java). There you will find plenty of working examples that you can use as a base for your own implementation.
### Adding more file icons for specific types
You can register file types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/resources/io/xpipe/app/resources/file_list.txt) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/browser_icons).
The existing file list and icons are taken from the [vscode-icons](https://github.com/vscode-icons/vscode-icons) project. Due to limitations in the file definition list compatibility, some file types might not be listed by their proper extension and are therefore not being applied correctly even though the images and definitions exist already.
### Adding more context menu actions in the file browser
In case you want to implement your own actions for certain file types in the file browser, you can easily do so. You can find most existing actions [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/browser) to get some inspiration.
Once you created your custom classes, you have to register them in your module info, just like [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/module-info.java).
### Implementing custom actions for the connection hub
All actions that you can perform for certain connections in the connection overview tab are implemented using an [Action API](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/ext/ActionProvider.java). You can find a sample implementation [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java) and many common action implementations [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/action).
### Familiarising yourself with the shell and command API
The [sample action](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java) shows the basics of working with shells and executing commands in them. For more references, just look for the usages of the [API classes](https://github.com/xpipe-io/xpipe/tree/master/core/src/main/java/io/xpipe/core/process).
### Implementing something else
if you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started.

View file

@ -5,14 +5,14 @@ There are no real formal contribution guidelines right now, they will maybe come
## Repository Structure
- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation
- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation.
This mainly concerns API classes not a lot of implementation.
- [beacon](beacon) - The XPipe beacon component is responsible for handling all communications between the XPipe
daemon
and the client applications, for example the various programming language APIs and the CLI
daemon and the client applications, for example APIs and the CLI
- [app](app) - Contains the XPipe daemon implementation, the XPipe desktop application, and an
API to create all different kinds of extensions for the XPipe platform
- [dist](dist) - Tools to create a distributable package of XPipe
- [ext](ext) - Available XPipe extensions. Essentially every feature is implemented as an extension
- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension
## Modularity
@ -21,8 +21,11 @@ All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [moditect](https://github.com/moditect/moditect-gradle-plugin).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 20)
is selected both for the project and for gradle itself.
## Setup
@ -44,8 +47,8 @@ You can use the gradle wrapper to build and run the project:
- `gradlew <project>:test` will run the tests of the specified project.
You are also able to properly debug the built production application through two different methods:
- The `app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it
- The `app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme).
- The `dist/build/dist/base/app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it
- The `dist/build/dist/base/app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme).
Just make sure that the attachme process is running within IntelliJ, and the debugger should launch automatically once you start up the application.
Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt

94
FAQ.md
View file

@ -5,18 +5,16 @@
Compared to other existing tools, the fundamental approach of how to
connect and how to communicate with remote systems differs.
Other tools utilize the established protocol-based approach, i.e. connect and communicate with a
server via a certain protocol like SSH, SFTP, and many more using an integrated library for that purpose.
XPipe utilizes a shell-based approach that works on top of command-line programs.
It interacts with your installed command-line programs via their stdout, stderr,
server via a certain protocol like SSH, SFTP, and many more using a bundled library for that purpose.
XPipe instead utilizes a shell-based approach that works on top of command-line programs.
It exclusively interacts with your installed command-line programs via their stdout, stderr,
and stdin to handle local and remote shell connections.
This approach makes it much more flexible as it doesn't have to deal with any file system APIs, remote file handling protocols, or libraries at all as that part is delegated to your existing programs.
Let's use the example of SSH.
Protocol-based programs come with an included SSH library that allows them to interact with a remote system via SSH.
This requires an SSH server implementation to be running on the remote system.
XPipe does not ship with any sort of SSH library or similar.
Instead, XPipe creates a new process using your local `ssh` executable, which is usually the OpenSSH client.
Instead, XPipe starts a new process using your local `ssh` executable, which is usually the OpenSSH client.
I.e. it launches the process `ssh user@host` in the background and communicates
with the opened remote shell through the stdout, stderr, stdin of the process.
From there, it detects what kind of server and environment,
@ -25,8 +23,7 @@ and adjusts how it talks to the remote system from there.
It effectively delegates everything protocol and connection related to your external programs.
As a result of this approach, you can do stuff with XPipe that you can't do with other tools.
One example would be connecting and accessing files on a
docker container as there's no real protocol to formally connect here by default.
One example would be connecting and accessing files on a docker container as there's no real protocol to formally connect here by default.
XPipe can simply execute `docker exec -i <name> sh` to open a shell into the container
and handle the file management through this opened shell by sending commands like `ls`, `touch`, and more.
@ -39,40 +36,33 @@ you can read the [introduction article](https://foojay.io/today/presenting-xpipe
## Does it run on my system?
The desktop application should run on any reasonably up-to-date
Windows/Linux/macOS system that has been released in the last ten years.
The desktop application should run on any reasonably up-to-date Windows/Linux/macOS system that has been released in the last ten years.
## What else do I need to use this?
As mentioned previously, XPipe itself does not ship with any sort of libraries for connection handling
and instead delegates this to your existing command-line tools.
For this approach to work however, you need to have the required tools installed.
For example, if you want to connect to a remote system via SSH with XPipe,
you need to have an `ssh` client installed and added to your PATH.
The exact vendor and version of this `ssh` command-line
tool doesn't matter as long as the standard options are supported.
If a required program is attempted to be used but can not be found, XPipe will notify you.
In case you are running this on a very slow system, there is also a performance mode available in the settings menu to reduce the visual fidelity and make the application more responsive.
## Is this secure / Can I entrust my sensitive information to this?
Due to its nature, XPipe has to handle a lot of sensitive information like passwords, keys, and more.
As security plays a very important role here, there exists a dedicated [security page](/SECURITY.md)
that should contain all relevant information for you to make your decision.
Due to its nature, XPipe has to handle a lot of sensitive information like passwords, keys, and more. As security plays a very important role here, there exists a dedicated [security details page](/SECURITY.md) that should contain all relevant information for you to make your decision.
In short, all is transferred in an encrypted manner to other programs. You can choose whether you want to store sensitive information within XPipe or source it from other sources such as password managers. If you store that sensitive data in XPipe, it is also stored encrypted on your local machine. You can also set a custom master password to improve the encryption security of your data further.
## How does XPipe handle privacy?
XPipe does not collect any sort of data like usage or tracking data.
The only case in which some sort of data is collected is when you choose to
use the built-in error reporter to submit a report.
XPipe does not collect any personal data.
The only case in which some sort of data is collected is when the built-in error reporter is used to submit a report.
This report data is limited to general system and error information, no sensitive information is submitted.
For those people who like to read legalese, there's the [privacy policy](/PRIVACY.md).
## How does XPipe handle updates?
## Do I have to pay to use this effectively?
Especially in its early development stage, it can be pretty important to frequently distribute new releases.
How exactly the update process is handled depends on your distribution:
I recently decided to develop XPipe full time and hope to finance this by providing plans for professional and commercial users.
The commercialization model is designed to be very generous for personal users. If you don't use XPipe for commercial purposes, you can use it basically without any limitations for free. If you intend to use it for commercial purposes or want to support the development, you can check out the [available tiers](https://buy.xpipe.io/checkout/buy/dbcd37b8-be94-40a5-8c1c-af61979e6537).
## Which release type should I choose?
You are able to essentially get the same feature set regardless which way you choose. There are a few small exceptions, such as desktop environment integrations for your operating system that are only available with installers, however these features are not crucial to XPipe.
Especially in its early development stage, it can be pretty important to frequently distribute new releases. How exactly the update process is handled depends on your distribution:
- Installers (msi/deb/rpm/pkg): They come with the ability to automatically check for
updates, download them, and install them if you provide your confirmation.
@ -83,17 +73,47 @@ How exactly the update process is handled depends on your distribution:
Note that you can choose to disable this update check functionality entirely in the settings menu.
## Does it matter which type of release I choose initially?
Not really, they all share the same configuration data locations. You can switch between different release types, e.g. from the portable version to an installer without any issues if you just want to try it out without installing.
There also exists a separate PTB (Public Test Build) release that is meant for testing out new features early on. You can find them at https://github.com/xpipe-io/xpipe-ptb if you're interested. The regular releases and PTB releases are designed to not interfere with each other and can therefore be installed and used side by side.
## How can I save/export my configuration data?
If you want to export or share the whole connection list, you can find all the data at ~/.xpipe/storage. You can also change that directory in the settings menu.
A simple solution is to change the storage directory to be in a cloud directory like OneDrive or Dropbox so it automatically synchronizes the data across all systems.
The professional version also comes with a feature to synchronize your storage with a remote git repository that you can host yourself wherever you like. This comes with the advantage of a commit history for individual connections and the ability to share this repository data with other team members using the access management of your git platform.
## Can I contribute to this project?
Yes, check out the [development page](/DEVELOPMENT.md) for details on how to set up a development environment and the [contributing page](/CONTRIBUTING.md) on how to get started.
## Why are there no GitHub actions workflows in this repository?
There are several test workflows run in a private environment as they use private test connections
such as remote server connections and database connections.
Other private workflows are responsible for packaging, signing, and distributing the releases.
So you can assume that the code is tested and the release is automated!
There are several test workflows run in a private environment as they use private test connections such as remote server connections and database connections. Other private workflows are responsible for packaging, signing, and distributing the releases and are also kept private due to them handling a lot of passwords and API keys. So you can assume that the code is tested and the release is automated!
## What is the best way to reach out to the developers and other users?
You can always open a GitHub issue in this repository in case you encounter a problem.
There are also several other ways to reach out, so you can choose whatever you like best:
You can always open a GitHub issue in this repository in case you encounter a problem. There are also several other ways to reach out, so you can choose whatever you like best:
- [XPipe Discord Server](https://discord.gg/8y89vS8cRb)
- [XPipe Slack Server](https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg)
## What is XPipe not?
XPipe is not:
- a backup tool: It is not designed to copy large masses of files across systems reliably.
- a system management tool: While it allows you to access any remote system, it does not come with a fancy management dashboard and overview for your server infrastructure.
- a terminal emulator: XPipe is designed around integrating with your own favorite terminal and will allow you to launch any preconfigured shell connection in it. It does not come with any integrated terminal functionality itself.
- a separate protocol handling implementation: XPipe does not come with its own libraries to handle protocols, so it is not able to connect via SSH without a locally installed SSH client like OpenSSH
- an RDP/VNC client: It does not support these protocols (yet)
## What will definitely not be implemented?
While the general development direction is still very open, there are a few things that definitely won't be implemented:
- A mobile version, an app store version, and a flatpak version: The concept of integrating with your local CLI tools is incompatible with most sandboxes.

View file

@ -1,293 +1,22 @@
**PRIVACY NOTICE (Created with termly.io)**
**PRIVACY NOTICE**
**Last updated April 21, 2023**
**Last updated September 17, 2023**
This privacy notice for XPipe, in which ("**we**," "**us**," or "**our**") refers to Christopher Schnick, describes how
This privacy notice for XPipe, in which ("**we**," "**us**," or "**our**") refers to XPipe UG (haftungsbeschränkt), describes how
and why we
might collect, store, use, and/or share ("**process**") your information when you use our services ("**Services**"),
such as when you:
* Download and use our application (XPipe), or any other application of ours that links to this privacy notice
* Download and use our application (XPipe)
**Questions or concerns?** Reading this privacy notice will help you understand your privacy rights and choices. If you
do not agree with our policies and practices, please do not use our Services. If you still have any questions or
concerns, please contact us at hello@xpipe.io.
<a name="toc"></a> **TABLE OF CONTENTS**
**1\. WHAT INFORMATION DO WE COLLECT?**
[1\. WHAT INFORMATION DO WE COLLECT?](#infocollect)
We do not process personal or sensitive information.
[2\. HOW DO WE PROCESS YOUR INFORMATION?](#infouse)
**2\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?**
[3\. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR PERSONAL INFORMATION?](#legalbases)
[4\. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?](#whoshare)
[5\. IS YOUR INFORMATION TRANSFERRED INTERNATIONALLY?](#intltransfers)
[6\. HOW LONG DO WE KEEP YOUR INFORMATION?](#inforetain)
[7\. HOW DO WE KEEP YOUR INFORMATION SAFE?](#infosafe)
[8\. WHAT ARE YOUR PRIVACY RIGHTS?](#privacyrights)
[9\. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?](#caresidents)
[10\. DO WE MAKE UPDATES TO THIS NOTICE?](#policyupdates)
[11\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)
[12\. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?](#request)
<a name="infocollect"></a> **1\. WHAT INFORMATION DO WE COLLECT?**
**Personal information you disclose to us**
We collect personal information that you voluntarily provide to us when you express an interest in obtaining information
about us or our products and Services, or otherwise when you contact us, for example by emailing us.
**Sensitive Information.** We do not process sensitive information.
**Error reports.** If you encounter and report application errors using the issue reporter,
we also may collect the following information if you choose to
provide us with access or permission:
* _Device Data._ Device information (such as your device ID, model, and
manufacturer), operating system, version information, and application identification numbers.
* _Log and Usage Data. (Optional choice)_ Log and usage data is service-related, diagnostic, usage, and performance information our
servers automatically collect when you access or use our Services and which we record in log files. Depending on how
you interact with us, this log data may include your IP address, device information, and settings and
information about your activity in the Services (such as the date/time stamps associated with your usage, pages and
files viewed, searches, and other actions you take such as which features you use), device event information (such as
system activity, error reports (sometimes called "crash dumps"), and hardware settings).
* _Error Diagnostics Information. (Optional choice)_ Information relating to a specific error that occurred in the application. This may
include application logs, event data, and any other kind of data that is used to diagnose the error.
This information is primarily needed to maintain the security and operation of our application(s), for troubleshooting,
and for our internal analytics and reporting purposes.
<a name="infouse"></a> **2\. HOW DO WE PROCESS YOUR INFORMATION?**
**We process your personal information for a variety of reasons, depending on how you interact with our Services,
including:**
* **To protect our Services.** We may process your information as part of our efforts to keep our Services safe and
secure, including error and exploit monitoring and prevention.
* **To save or protect an individual's vital interest.** We may process your information when necessary to save or
protect an individuals vital interest, such as to prevent harm.
<a name="legalbases"></a> **3\. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR INFORMATION?**
_**In Short:** We only process your personal information when we believe it is necessary and we have a valid legal
reason (i.e., legal basis) to do so under applicable law, like with your consent, to comply with laws, to provide you
with services to enter into or fulfill our contractual obligations, to protect your rights, or to fulfill our legitimate
business interests._
_**If you are located in the EU or UK, this section applies to you.**_
The General Data Protection Regulation (GDPR) and UK GDPR require us to explain the valid legal bases we rely on in
order to process your personal information. As such, we may rely on the following legal bases to process your personal
information:
* **Consent.** We may process your information if you have given us permission (i.e., consent) to use your personal
information for a specific purpose. You can withdraw your consent at any time. Click [here](#request) to learn
more.
* **Legitimate Interests.** We may process your information when we believe it is reasonably necessary to achieve our
legitimate business interests and those interests do not outweigh your interests and fundamental rights and freedoms.
For example, we may process your personal information for some of the purposes described in order to:
* Diagnose problems and/or prevent errors and exploits
* Understand how our users use our products and services so we can improve user experience
* **Legal Obligations.** We may process your information where we believe it is necessary for compliance with our legal
obligations, such as to cooperate with a law enforcement body or regulatory agency, exercise or defend our legal
rights, or disclose your information as evidence in litigation in which we are involved.
* **Vital Interests.** We may process your information where we believe it is necessary to protect your vital interests
or the vital interests of a third party, such as situations involving potential threats to the safety of any person.
In legal terms, we are generally the "data controller" under European data protection laws of the personal information
described in this privacy notice, since we determine the means and/or purposes of the data processing we perform. This
privacy notice does not apply to the personal information we process as a "data processor" on behalf of our customers.
In those situations, the customer that we provide services to and with whom we have entered into a data processing
agreement is the "data controller" responsible for your personal information, and we merely process your information on
their behalf in accordance with your instructions. If you want to know more about our customers' privacy practices, you
should read their privacy policies and direct any questions you have to them.
**_If you are located in Canada, this section applies to you._**
We may process your information if you have given us specific permission (i.e., express consent) to use your personal
information for a specific purpose, or in situations where your permission can be inferred (i.e., implied consent). You
can withdraw your consent at any time. Click [here](#request) to learn more.
In some exceptional cases, we may be legally permitted under applicable law to process your information without your
consent, including, for example:
* If collection is clearly in the interests of an individual and consent cannot be obtained in a timely way
* For investigations and fraud detection and prevention
* For business transactions provided certain conditions are met
* If it is contained in a witness statement and the collection is necessary to assess, process, or settle an insurance
claim
* For identifying injured, ill, or deceased persons and communicating with next of kin
* If we have reasonable grounds to believe an individual has been, is, or may be victim of financial abuse
* If it is reasonable to expect collection and use with consent would compromise the availability or the accuracy of the
information and the collection is reasonable for purposes related to investigating a breach of an agreement or a
contravention of the laws of Canada or a province
* If disclosure is required to comply with a subpoena, warrant, court order, or rules of the court relating to the
production of records
* If it was produced by an individual in the course of their employment, business, or profession and the collection is
consistent with the purposes for which the information was produced
* If the collection is solely for journalistic, artistic, or literary purposes
* If the information is publicly available and is specified by the regulations
<a name="whoshare"></a> **4\. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?**
**_In Short:_** _We may share information in specific situations described in this section and/or with the following
third parties._
* **Error reporting and usage tracking**
**_Sentry_**:
Functional Software, Inc. dba Sentry, 45 Fremont Street, 8th Floor, San Francisco, CA 94105.
You can find their privacy policy here: [https://sentry.io/privacy/](https://sentry.io/privacy/)
<a name="intltransfers"></a> **5\. IS YOUR INFORMATION TRANSFERRED INTERNATIONALLY?**
**_In Short:_** _We may transfer, store, and process your information in countries other than your own._
Please be aware that your information may be transferred to, stored, and processed by us in our
facilities and by those third parties with whom we may share your personal information (
see "[WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?](#whoshare)" above), in the United States, and other
countries.
If you are a resident in the European Economic Area (EEA) or United Kingdom (UK), then these countries may not
necessarily have data protection laws or other similar laws as comprehensive as those in your country. However, we will
take all necessary measures to protect your personal information in accordance with this privacy notice and applicable
law.
European Commission's Standard Contractual Clauses:
We have implemented measures to protect your personal information, including by using the European Commission's Standard
Contractual Clauses for transfers of personal information between our group companies and between us and our third-party
providers. These clauses require all recipients to protect all personal information that they process originating from
the EEA or UK in accordance with European data protection laws and regulations. Our Data Processing Agreements that
include Standard Contractual Clauses are available here: [https://sentry.io/legal/dpa/](https://sentry.io/legal/dpa/).
We have implemented similar appropriate safeguards with our third-party service providers and partners and further
details can be provided upon request.
<a name="inforetain"></a> **6\. HOW LONG DO WE KEEP YOUR INFORMATION?**
**_In Short:_** _We keep your information for as long as necessary to fulfill the purposes outlined in this privacy
notice unless otherwise required by law._
We will only keep your personal information for as long as it is necessary for the purposes set out in this privacy
notice, unless a longer retention period is required or permitted by law (such as tax, accounting, or other legal
requirements).
When we have no ongoing legitimate business need to process your personal information, we will either delete or
anonymize such information, or, if this is not possible (for example, because your personal information has been stored
in backup archives), then we will securely store your personal information and isolate it from any further processing
until deletion is possible.
<a name="infosafe"></a> **7\. HOW DO WE KEEP YOUR INFORMATION SAFE?**
**_In Short:_** _We aim to protect your personal information through a system of organizational and technical security
measures._
We have implemented appropriate and reasonable technical and organizational security measures designed to protect the
security of any personal information we process. However, despite our safeguards and efforts to secure your information,
no electronic transmission over the Internet or information storage technology can be guaranteed to be 100% secure, so
we cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to
defeat our security and improperly collect, access, steal, or modify your information. Although we will do our best to
protect your personal information, transmission of personal information to and from our Services is at your own risk.
You should only access the Services within a secure environment.
<a name="privacyrights"></a> **8\. WHAT ARE YOUR PRIVACY RIGHTS?**
**_In Short:_** _In some regions, such as the European Economic Area (EEA), United Kingdom (UK), and Canada, you have
rights that allow you greater access to and control over your personal information. You may review, change, or terminate
your account at any time._
In some regions (like the EEA, UK, and Canada), you have certain rights under applicable data protection laws. These may
include the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or
erasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In
certain circumstances, you may also have the right to object to the processing of your personal information. You can
make such a request by contacting us by using the contact details provided in the
section "[HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)" below.
We will consider and act upon any request in accordance with applicable data protection laws.
If you are located in the EEA or UK and you believe we are unlawfully processing your personal information, you also
have the right to complain to your local data protection supervisory authority. You can find their contact details
here: [https://ec.europa.eu/justice/data-protection/bodies/authorities/index\_en.htm](https://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm)
.
If you are located in Switzerland, the contact details for the data protection authorities are available
here: [https://www.edoeb.admin.ch/edoeb/en/home.html](https://www.edoeb.admin.ch/edoeb/en/home.html).
**Withdrawing your consent:** If we are relying on your consent to process your personal information, which may be
express and/or implied consent depending on the applicable law, you have the right to withdraw your consent at any time.
You can withdraw your consent at any time by contacting us by using the contact details provided in the
section "[HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)" below.
However, please note that this will not affect the lawfulness of the processing before its withdrawal nor, when
applicable law allows, will it affect the processing of your personal information conducted in reliance on lawful
processing grounds other than consent.
If you have questions or comments about your privacy rights, you may email us at hello@xpipe.io.
<a name="caresidents"></a> **9\. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?**
**_In Short:_** _Yes, if you are a resident of California, you are granted specific rights regarding access to your
personal information._
California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users who are California
residents to request and obtain from us, once a year and free of charge, information about categories of personal
information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all
third parties with which we shared personal information in the immediately preceding calendar year. If you are a
California resident and would like to make such a request, please submit your request in writing to us using the contact
information provided below.
If you are under 18 years of age, reside in California, and have a registered account with Services, you have the right
to request removal of unwanted data that you publicly post on the Services. To request removal of such data, please
contact us using the contact information provided below and include the email address associated with your account and a
statement that you reside in California. We will make sure the data is not publicly displayed on the Services, but
please be aware that the data may not be completely or comprehensively removed from all our systems (e.g., backups,
etc.).
<a name="policyupdates"></a> **10\. DO WE MAKE UPDATES TO THIS NOTICE?**
_**In Short:** Yes, we will update this notice as necessary to stay compliant with relevant laws._
We may update this privacy notice from time to time. The updated version will be indicated by an updated "Revised" date
and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy
notice, we may notify you either by prominently posting a notice of such changes or by directly sending you a
notification. We encourage you to review this privacy notice frequently to be informed of how we are protecting your
information.
<a name="contact"></a> **11\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?**
If you have questions or comments about this notice, you may contact Christopher
Schnick by email at crschnick@xpipe.io.
<a name="request"></a> **12\. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?**
Based on the applicable laws of your country, you may have the right to request access to the personal information we
collect from you, change that information, or delete it. To request to review, update, or delete your personal
information, please submit a request form by writing to hello@xpipe.io.
If you have questions or comments, you may contact us by email at hello@xpipe.io.

View file

@ -9,23 +9,18 @@ XPipe fully integrates with your tools such as your favourite text/code editors,
It currently supports:
- [Kubernetes](https://kubernetes.io/) clusters, pods, and containers
- [Docker](https://www.docker.com/), [Podman](https://podman.io/), and [LXD](https://linuxcontainers.org/lxd/introduction/) container instances located on any host
- [SSH](https://www.ssh.com/academy/ssh/protocol) connections, config file connections, and tunnels
- [SSH](https://www.ssh.com/academy/ssh/protocol) connections, config files, and tunnels
- [Windows Subsystem for Linux](https://ubuntu.com/wsl), [Cygwin](https://www.cygwin.com/), and [MSYS2](https://www.msys2.org/) instances
- [Powershell Remote Sessions](https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.3)
- Any other custom remote connection methods that work through the command-line
Furthermore, you can also use any remote shell connection as a proxy when establishing new connections, allowing full flexibility to set up connection routes.
The project is still in a relatively early stage and will benefit massively from your feedback, issue reports, feature request, and more. There are also a lot more features to come in the future.
You have more questions? Then check out the new [FAQ](/FAQ.md).
## Connection Hub
- Easily connect to and access all kinds of remote connections in one place
- Securely stores all information exclusively on your computer and encrypts all secret information. See the [security page](/SECURITY.md) for more information
- Allows you to fully customize the init environment of the launched shell sessions with custom scripts
- Allows you to create specific login environments on any system to instantly jump into proper environment for every use case of yours
- Can create desktop shortcuts that automatically open remote connections in your terminal
- Group all your connections into hierarchical categories
![connections](https://github.com/xpipe-io/xpipe/assets/72509152/ef19aa85-1b66-45e0-a051-5a4658758626)
@ -34,10 +29,9 @@ You have more questions? Then check out the new [FAQ](/FAQ.md).
- Interact with the file system of any remote system using a workflow optimized for professionals
- Quickly open a terminal into any directory
- Utilize your favourite local programs to open and edit remote files
- Has the same feature set for all supported connection types
- Dynamically elevate sessions with sudo when required
The feature set is the same for all supported connection types. It of course also supports browsing the file system on your local machine.
The feature set is the same for all supported connection types. It also supports browsing the file system on your local machine.
![browser](https://github.com/xpipe-io/xpipe/assets/72509152/5631fe50-58b4-4847-a5f4-ad3898a02a9f)
@ -100,12 +94,21 @@ bash <(curl -sL https://raw.githubusercontent.com/xpipe-io/xpipe/master/get-xpip
powershell -ExecutionPolicy Bypass -Command iwr "https://raw.githubusercontent.com/xpipe-io/xpipe/master/get-xpipe.ps1" -OutFile "$env:TEMP\get-xpipe.ps1" ";" "&" "$env:TEMP\get-xpipe.ps1"
```
## Tiers
Recently I decided to try to develop XPipe full-time. To finance this, there now is a professional tier intended for commercial users.
The free community tier comes with the full feature set and no restrictions on anything as long you are using it for non-commercial purposes. For commercial usage, I would like to ask you to purchase an [XPipe professional license](https://buy.xpipe.io/checkout/buy/dbcd37b8-be94-40a5-8c1c-af61979e6537). You can try out XPipe as much as you want in a non-commercial setting or start a free trial if you want to test it in your commercial environments prior to purchasing a license.
## Open source model
XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future. This mainly concerns the shell handling library implementation and extensions for configuring and handling shell connections. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. Finally, scripts and workflows to create and publish installers and packages are also not included to prevent attackers from easily impersonating the XPipe application.
XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future.
This mainly concerns the features only available in the professional tier and the shell handling library implementation and extensions for configuring and handling shell connections. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. Finally, scripts and workflows to create and publish installers and packages are also not included to prevent attackers from easily impersonating the XPipe application.
## Further information
You have more questions? Then check out the new [FAQ](/FAQ.md).
For information about the security model of XPipe, see the [security page](/SECURITY.md).
For information about the privacy policy of XPipe, see the [privacy page](/PRIVACY.md).

View file

@ -132,7 +132,7 @@ public final class XPipeApiConnection extends BeaconConnection {
}
private void start() throws Exception {
var installation = XPipeInstallation.getLocalDefaultInstallationBasePath(true);
var installation = XPipeInstallation.getLocalDefaultInstallationBasePath();
BeaconServer.start(installation, XPipeDaemonMode.TRAY);
}

View file

@ -74,7 +74,7 @@ apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"
sourceSets {
main {
output.resourcesDir("$buildDir/classes/java/main")
output.resourcesDir("${project.layout.buildDirectory.get()}/classes/java/main")
}
}
@ -100,6 +100,7 @@ List<String> jvmRunArgs = [
"--add-exports", "javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls",
"--add-exports", "javafx.graphics/com.sun.javafx.scene=org.controlsfx.controls",
"--add-exports", "org.apache.commons.lang3/org.apache.commons.lang3.math=io.xpipe.app",
"--add-opens", "java.base/java.lang=io.xpipe.app",
"--add-opens", "java.base/java.lang.reflect=com.jfoenix",
"--add-opens", "java.base/java.lang.reflect=com.jfoenix",
"--add-opens", "java.base/java.lang=io.xpipe.core",
@ -110,9 +111,9 @@ List<String> jvmRunArgs = [
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.model=io.xpipe.app',
"-Xmx8g",
"-Dio.xpipe.app.arch=$rootProject.arch",
"--enable-preview",
// "-XX:+ExitOnOutOfMemoryError",
"-Dfile.encoding=UTF-8",
'-XX:+UseZGC',
"-Dvisualvm.display.name=XPipe"
]
@ -137,7 +138,7 @@ application {
run {
systemProperty 'io.xpipe.app.useVirtualThreads', 'false'
systemProperty 'io.xpipe.app.mode', 'gui'
systemProperty 'io.xpipe.app.dataDir', "$projectDir/local7/"
systemProperty 'io.xpipe.app.dataDir', "$projectDir/local_git2/"
systemProperty 'io.xpipe.app.writeLogs', "true"
systemProperty 'io.xpipe.app.writeSysOut', "true"
systemProperty 'io.xpipe.app.developerMode', "true"

View file

@ -1,21 +1,21 @@
dependencies {
implementation files("$buildDir/generated-modules/flexmark-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-data-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-ast-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-builder-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-sequence-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-misc-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-dependency-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-collection-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-format-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-html-0.64.0.jar")
implementation files("$buildDir/generated-modules/flexmark-util-visitor-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-data-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-ast-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-builder-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-sequence-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-misc-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-dependency-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-collection-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-format-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-html-0.64.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-visitor-0.64.0.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("$buildDir/generated-modules")
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact 'com.vladsch.flexmark:flexmark:0.64.0'

View file

@ -1,11 +1,11 @@
dependencies {
implementation files("$buildDir/generated-modules/github-api-1.301.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/github-api-1.301.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("$buildDir/generated-modules")
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact 'org.kohsuke:github-api:1.301'

View file

@ -1,15 +1,15 @@
dependencies {
implementation files("$buildDir/generated-modules/richtextfx-0.10.6.jar")
implementation files("$buildDir/generated-modules/flowless-0.6.6.jar")
implementation files("$buildDir/generated-modules/undofx-2.1.1.jar")
implementation files("$buildDir/generated-modules/wellbehavedfx-0.3.3.jar")
implementation files("$buildDir/generated-modules/reactfx-2.0-M5.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/richtextfx-0.10.6.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/flowless-0.6.6.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/undofx-2.1.1.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/wellbehavedfx-0.3.3.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/reactfx-2.0-M5.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("$buildDir/generated-modules")
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6.6'

View file

@ -1,14 +1,14 @@
dependencies {
implementation files("$buildDir/generated-modules/sentry-6.16.0.jar")
implementation files("${project.layout.buildDirectory.get()}/generated-modules/sentry-6.29.0.jar")
}
addDependenciesModuleInfo {
overwriteExistingFiles = true
jdepsExtraArgs = ['-q']
outputDirectory = file("$buildDir/generated-modules")
outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules")
modules {
module {
artifact 'io.sentry:sentry:6.16.0'
artifact 'io.sentry:sentry:6.29.0'
moduleInfoSource = '''
module io.sentry {
exports io.sentry;

View file

@ -1,39 +1,41 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.storage.store.StoreEntryTree;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.comp.storage.store.StoreSection;
import io.xpipe.app.comp.storage.store.StoreSectionMiniComp;
import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedHierarchyStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.SetChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Predicate;
final class BrowserBookmarkList extends SimpleComp {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public static final Timer DROP_TIMER = new Timer("dnd", true);
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
@ -46,167 +48,68 @@ final class BrowserBookmarkList extends SimpleComp {
@Override
protected Region createSimple() {
var root = StoreEntryTree.createTree();
var view = new TreeView<>(root);
view.setShowRoot(false);
view.getStyleClass().add("bookmark-list");
view.setCellFactory(param -> {
return new StoreCell(view);
});
PlatformThread.sync(model.getSelected()).addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
view.getSelectionModel().clearSelection();
return;
}
view.getSelectionModel()
.select(getTreeViewItem(
root,
StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> storeEntryWrapper
.getState()
.getValue()
.isUsable()
&& storeEntryWrapper
.getEntry()
.getStore()
.equals(newValue.getStore()))
.findAny()
.orElse(null)));
});
return view;
}
private static TreeItem<StoreEntryWrapper> getTreeViewItem(
TreeItem<StoreEntryWrapper> item, StoreEntryWrapper value) {
if (item.getValue() != null && item.getValue().equals(value)) {
return item;
}
for (TreeItem<StoreEntryWrapper> child : item.getChildren()) {
TreeItem<StoreEntryWrapper> s = getTreeViewItem(child, value);
if (s != null) {
return s;
}
}
return null;
}
private final class StoreCell extends TreeCell<StoreEntryWrapper> {
private final StringProperty img = new SimpleStringProperty();
private final Node imageView = new PrettyImageComp(img, 20, 20).createRegion();
private final BooleanProperty busy = new SimpleBooleanProperty(false);
@Override
protected double computePrefWidth(double height) {
// This makes the cell always properly cut of any overflow of text
return 1;
}
private StoreCell(TreeView<?> t) {
disableProperty().bind(busy);
setAccessibleRole(AccessibleRole.BUTTON);
setGraphic(imageView);
setTextOverrun(OverrunStyle.ELLIPSIS);
addEventHandler(DragEvent.DRAG_OVER, mouseEvent -> {
if (getItem() == null) {
return;
}
// Disable this for now
// handleHoverTimer(getItem().getEntry().getStore(), mouseEvent);
mouseEvent.consume();
});
addEventHandler(DragEvent.DRAG_EXITED, mouseEvent -> {
activeTask = null;
mouseEvent.consume();
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (getItem() == null
|| event.getButton() != MouseButton.PRIMARY
|| (!getItem().getState().getValue().isUsable())) {
return;
}
ThreadHelper.runFailableAsync(() -> {
if (getItem().getEntry().getStore() instanceof ShellStore fileSystem) {
BusyProperty.execute(busy, () -> {
getItem().refreshIfNeeded();
var filterText = new SimpleStringProperty();
var open = PlatformThread.sync(model.getSelected());
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
&& storeEntryWrapper.getEntry().getState().isUsable();
};
var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), applicable, filterText, StoreViewState.get().getActiveCategory()),
(s, comp) -> {
BooleanProperty busy = new SimpleBooleanProperty(false);
comp.disable(Bindings.createBooleanBinding(() -> {
return busy.get() || !applicable.test(s.getWrapper());
}, busy)).apply(struc -> {
open.addListener((observable, oldValue, newValue) -> {
struc.get()
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.getStore()
.equals(s.getWrapper()
.getEntry()
.getStore()));
});
model.openFileSystemAsync(null, fileSystem, null, busy);
} else if (getItem().getEntry().getStore() instanceof FixedHierarchyStore) {
BusyProperty.execute(busy, () -> {
getItem().refreshWithChildren();
struc.get().setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
var entry = s.getWrapper().getEntry();
if (entry.getStore() instanceof ShellStore fileSystem) {
BooleanScope.execute(busy, () -> {
s.getWrapper().refreshIfNeeded();
});
model.openFileSystemAsync(null, fileSystem, null, busy);
} else if (entry.getStore() instanceof FixedHierarchyStore) {
BooleanScope.execute(busy, () -> {
s.getWrapper().refreshWithChildren();
});
}
});
event.consume();
});
}
});
});
event.consume();
});
var icon = new SimpleObjectProperty<>("mdal-keyboard_arrow_right");
getPseudoClassStates().addListener((SetChangeListener<? super PseudoClass>) change -> {
if (change.getSet().contains(PseudoClass.getPseudoClass("expanded"))) {
icon.set("mdal-keyboard_arrow_down");
} else {
icon.set("mdal-keyboard_arrow_right");
}
});
var button = new IconButtonComp(icon, () -> {
getTreeItem().setExpanded(!getTreeItem().isExpanded());
})
.apply(struc -> struc.get().setPrefWidth(25))
.grow(false, true)
.styleClass("expand-button")
.apply(struc -> struc.get().setFocusTraversable(false));
setDisclosureNode(button.createRegion());
var category = new DataStoreCategoryChoiceComp(StoreViewState.get().getActiveCategory())
.styleClass(Styles.LEFT_PILL)
.grow(false, true);
var filter =
new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
indexProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.intValue() == 0) {
getStyleClass().add("first");
} else {
getStyleClass().remove("first");
}
});
}
var top = new HorizontalComp(List.of(category, filter.hgrow()))
.styleClass("top")
.apply(struc -> {
AppFont.medium(struc.get());
struc.get().setFillHeight(true);
})
.createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);
@Override
public void updateItem(StoreEntryWrapper item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
// Use opacity instead of visibility as visibility is kinda bugged with web views
setOpacity(0.0);
setFocusTraversable(false);
setAccessibleText(null);
} else {
setText(item.getName());
// Check if store is in failed state
if (item.getEntry().getState() == DataStoreEntry.State.LOAD_FAILED) {
setGraphic(null);
setFocusTraversable(false);
setAccessibleText(null);
return;
}
img.set(item.getEntry()
.getProvider()
.getDisplayIconFileName(item.getEntry().getStore()));
setOpacity(1.0);
setFocusTraversable(true);
setAccessibleText(
item.getName() + " " + item.getEntry().getProvider().getDisplayName());
}
}
content.getStyleClass().add("bookmark-list");
return content;
}
private void handleHoverTimer(DataStore store, DragEvent event) {

View file

@ -1,6 +1,8 @@
package io.xpipe.app.browser;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.ClipboardContent;
@ -8,6 +10,11 @@ import javafx.scene.input.Dragboard;
import lombok.SneakyThrows;
import lombok.Value;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -24,6 +31,41 @@ public class BrowserClipboard {
public static final Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
public static Instance currentDragClipboard;
static {
Toolkit.getDefaultToolkit()
.getSystemClipboard()
.addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<Throwable>() {
@Override
@SuppressWarnings("unchecked")
public void run() throws Throwable {
Clipboard clipboard = (Clipboard) e.getSource();
try {
if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) {
return;
}
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
var files = data.stream().map(string -> string.toPath()).toList();
if (files.size() == 0) {
return;
}
var entries = new ArrayList<FileSystem.FileEntry>();
for (Path file : files) {
entries.add(FileSystemHelper.getLocal(file));
}
currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries));
} catch (IllegalStateException ex) {
// Handle awt bug
if (!"cannot open system clipboard".equals(ex.getMessage())) {
throw ex;
}
}
}
}));
}
@SneakyThrows
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
var content = new ClipboardContent();

View file

@ -10,13 +10,12 @@ import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@ -34,6 +33,7 @@ import javafx.scene.layout.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
@ -88,7 +88,7 @@ public class BrowserComp extends SimpleComp {
.widthProperty()
.addListener(
// set sidebar width in pixels depending on split pane width
(obs, old, val) -> splitPane.setDividerPosition(0, 320 / splitPane.getWidth()));
(obs, old, val) -> splitPane.setDividerPosition(0, 360 / splitPane.getWidth()));
var r = addBottomBar(splitPane);
r.getStyleClass().add("browser");
@ -149,7 +149,8 @@ public class BrowserComp extends SimpleComp {
private TabPane createTabPane() {
var tabs = new TabPane();
tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE);
tabs.setTabMinWidth(Region.USE_PREF_SIZE);
tabs.setTabMaxWidth(400);
tabs.setTabClosingPolicy(ALL_TABS);
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
@ -200,7 +201,7 @@ public class BrowserComp extends SimpleComp {
while (c.next()) {
for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> {
try (var b = new BusyProperty(modifying)) {
try (var b = new BooleanScope(modifying).start()) {
var t = map.remove(r);
tabs.getTabs().remove(t);
}
@ -209,7 +210,7 @@ public class BrowserComp extends SimpleComp {
for (var a : c.getAddedSubList()) {
PlatformThread.runLaterIfNeeded(() -> {
try (var b = new BusyProperty(modifying)) {
try (var b = new BooleanScope(modifying).start()) {
var t = createTab(tabs, a);
map.put(a, t);
tabs.getTabs().add(t);
@ -244,34 +245,51 @@ public class BrowserComp extends SimpleComp {
var tab = new Tab();
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(14, 14);
ring.setPrefSize(14, 14);
ring.setMaxSize(14, 14);
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var image = DataStoreProviders.byStore(model.getStore()).getDisplayIconFileName(model.getStore());
var logo = new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion();
var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion();
var label = new Label(model.getName());
label.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
label.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab)));
label.graphicProperty()
tab.graphicProperty()
.bind(Bindings.createObjectBinding(
() -> {
return model.getBusy().get() ? ring : logo;
},
PlatformThread.sync(model.getBusy())));
tab.setGraphic(label);
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(label);
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(label));
tab.setContent(new OpenFileSystemComp(model).createSimple());
tab.setText(model.getName());
// new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(tab);
tab.setContent(new OpenFileSystemComp(model).createSimple());
var id = UUID.randomUUID().toString();
tab.setId(id);
var found = tabs.lookupAll("tab-header-area");
SimpleChangeListener.apply(tabs.skinProperty(), newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
Label l = (Label) tabs.lookup("#" + id + " .tab-label");
var w = l.maxWidthProperty();
l.minWidthProperty().bind(w);
l.prefWidthProperty().bind(w);
var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button");
close.setPrefWidth(30);
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab)));
});
}
});
return tab;
}
}

View file

@ -7,9 +7,9 @@ import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.SvgCacheComp;
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
@ -423,8 +423,7 @@ final class BrowserFileListComp extends SimpleComp {
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final Node imageView = new SvgCacheComp(
new SimpleDoubleProperty(24), new SimpleDoubleProperty(24), img, FileIconManager.getSvgCache())
private final Node imageView = new PrettySvgComp(img, 24, 24)
.createRegion();
private final StackPane textField =
new LazyTextFieldComp(text).createStructure().get();
@ -473,7 +472,7 @@ final class BrowserFileListComp extends SimpleComp {
return;
}
try (var ignored = new BusyProperty(updating)) {
try (var ignored = new BooleanScope(updating).start()) {
super.updateItem(newName, empty);
if (empty || newName == null || getTableRow().getItem() == null) {
// Don't set image as that would trigger image comp update

View file

@ -120,7 +120,7 @@ public final class BrowserFileListModel {
}
if (exists) {
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").unreportable().handle();
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle();
fileSystemModel.refresh();
return false;
}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.ShellStore;
@ -78,8 +78,7 @@ public class BrowserModel {
public void restoreState(BrowserSavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntry(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(
entry.getName(), entry.getStore().asNeeded(), e.getPath(), busy);
openFileSystemAsync(null, entry.getStore().asNeeded(), e.getPath(), busy);
});
}
@ -177,7 +176,7 @@ public class BrowserModel {
// Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) {
try (var b = new BusyProperty(externalBusy != null ? externalBusy : new SimpleBooleanProperty())) {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(name, this, store);
model.initFileSystem();
model.initSavedState();

View file

@ -6,12 +6,9 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@ -46,7 +43,7 @@ public class BrowserNavBar extends SimpleComp {
});
path.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(model.getBusy(), () -> {
BooleanScope.execute(model.getBusy(), () -> {
var changed = model.cdSyncOrRetry(newValue, true);
changed.ifPresent(s -> Platform.runLater(() -> path.set(s)));
});
@ -94,7 +91,7 @@ public class BrowserNavBar extends SimpleComp {
: "home_icon.svg";
},
model.getCurrentPath());
var breadcrumbsGraphic = new PrettyImageComp(graphic, 22, 22)
var breadcrumbsGraphic = PrettyImageHelper.ofSvg(graphic, 22, 22)
.padding(new Insets(0, 0, 1, 0))
.styleClass("path-graphic")
.createRegion();

View file

@ -6,11 +6,10 @@ import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.SvgCacheComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
@ -19,7 +18,6 @@ import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import lombok.AllArgsConstructor;
@ -54,11 +52,7 @@ public class BrowserSelectionListComp extends SimpleComp {
protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> {
return Comp.of(() -> {
var wv = new SvgCacheComp(
new SimpleDoubleProperty(20),
new SimpleDoubleProperty(20),
new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)),
FileIconManager.getSvgCache())
var wv = PrettyImageHelper.ofFixedSquare(FileIconManager.getFileIcon(entry, false), 20)
.createRegion();
var l = new Label(null, wv);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);

View file

@ -4,7 +4,7 @@ import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragPseudoClassAugment;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
@ -82,7 +82,7 @@ public class BrowserTransferComp extends SimpleComp {
var listBox = new VerticalComp(List.of(list, dragNotice)).padding(new Insets(10, 10, 5, 10));
var stack = new LoadingOverlayComp(
new StackComp(List.of(backgroundStack, listBox, clearPane))
.apply(DragPseudoClassAugment.create())
.apply(DragOverPseudoClassAugment.create())
.apply(struc -> {
struc.get().setOnDragOver(event -> {
// Accept drops from inside the app window

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.BooleanProperty;
@ -82,7 +82,7 @@ public class BrowserTransferModel {
}
try {
try (var b = new BusyProperty(downloading)) {
try (var b = new BooleanScope(downloading).start()) {
FileSystemHelper.dropFilesInto(
FileSystemHelper.getLocal(TEMP),
List.of(item.getFileEntry()),

View file

@ -1,23 +1,22 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.Tile;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserWelcomeComp extends SimpleComp {
@ -33,15 +32,18 @@ public class BrowserWelcomeComp extends SimpleComp {
var welcome = new BrowserGreetingComp().createSimple();
var vbox = new VBox(welcome);
vbox.setMaxWidth(700);
vbox.setPadding(new Insets(40, 40, 40, 50));
vbox.setSpacing(18);
var vbox = new VBox(welcome, new Spacer(Orientation.VERTICAL));
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75).padding(new Insets(5, 0, 0, 0)).createRegion();
var hbox = new HBox(img, vbox);
hbox.setSpacing(15);
if (state == null) {
var header = new Label("Here you will be able to see were you left off last time you exited XPipe.");
var header = new Label("Here you will be able to see where you left off last time you exited XPipe.");
AppFont.header(header);
vbox.getChildren().add(header);
return vbox;
hbox.setPadding(new Insets(40, 40, 40, 50));
return new VBox(hbox);
}
var header = new Label("Last time you were connected to the following systems:");
@ -51,7 +53,7 @@ public class BrowserWelcomeComp extends SimpleComp {
var storeList = new VBox();
storeList.setSpacing(8);
state.getLastSystems().forEach(e-> {
state.getLastSystems().forEach(e -> {
var entry = DataStorage.get().getStoreEntry(e.getUuid());
if (entry.isEmpty()) {
return;
@ -63,30 +65,36 @@ public class BrowserWelcomeComp extends SimpleComp {
var graphic =
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = new PrettyImageComp(new SimpleStringProperty(graphic), 45, 45);
var openButton = new Button(null, new FontIcon("mdmz-restore"));
new FancyTooltipAugment<>("restore").augment(openButton);
openButton.getStyleClass().addAll(Styles.FLAT, Styles.BUTTON_CIRCLE);
openButton.setOnAction(event -> {
model.restoreState(e, openButton.disableProperty());
event.consume();
var view = PrettyImageHelper.ofFixedSquare(graphic, 45);
view.padding(new Insets(2, 8, 2, 8));
var tile = new Tile(
DataStorage.get().getStoreBrowserDisplayName(entry.get().getStore()),
e.getPath(),
view.createRegion());
tile.setActionHandler(() -> {
model.restoreState(e, tile.disableProperty());
});
var tile = new Tile(entry.get().getName(), e.getPath(), view.createRegion());
tile.setAction(openButton);
storeList.getChildren().add(tile);
});
var sp = new ScrollPane(storeList);
sp.setFitToWidth(true);
vbox.getChildren().add(sp);
vbox.getChildren().add(new Separator(Orientation.HORIZONTAL));
var layout = new VBox();
layout.setMaxWidth(700);
layout.setPadding(new Insets(40, 40, 40, 50));
layout.setSpacing(18);
layout.getChildren().add(hbox);
layout.getChildren().add(new Separator(Orientation.HORIZONTAL));
layout.getChildren().add(sp);
layout.getChildren().add(new Separator(Orientation.HORIZONTAL));
var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> {
model.restoreState(state);
actionEvent.consume();
}).grow(true, false);
vbox.getChildren().add(tile.createRegion());
layout.getChildren().add(tile.createRegion());
return vbox;
return layout;
}
}

View file

@ -3,18 +3,18 @@ package io.xpipe.app.browser;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import lombok.Getter;
import lombok.SneakyThrows;
import org.apache.commons.lang3.function.FailableConsumer;
import java.io.IOException;
import java.nio.file.Path;
@ -46,7 +46,7 @@ public final class OpenFileSystemModel {
this.browserModel = browserModel;
this.store = store;
var e = DataStorage.get().getStoreEntryIfPresent(store);
this.name = name != null ? name : e.isPresent() ? e.get().getName() : "?";
this.name = name != null ? name : e.isPresent() ? DataStorage.get().getStoreBrowserDisplayName(store) : "?";
this.tooltip = e.isPresent() ? DataStorage.get().getId(e.get()).toString() : name;
this.inOverview.bind(Bindings.createBooleanBinding(
() -> {
@ -62,7 +62,7 @@ public final class OpenFileSystemModel {
return;
}
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (store instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
if (refresh) {
@ -75,7 +75,7 @@ public final class OpenFileSystemModel {
@SneakyThrows
public void refresh() {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
cdSyncWithoutCheck(currentPath.get());
});
}
@ -112,7 +112,7 @@ public final class OpenFileSystemModel {
public void cdAsync(String path) {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
cdSync(path);
});
});
@ -238,7 +238,7 @@ public final class OpenFileSystemModel {
public void dropLocalFilesIntoAsync(FileSystem.FileEntry entry, List<Path> files) {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (fileSystem == null) {
return;
}
@ -257,7 +257,7 @@ public final class OpenFileSystemModel {
}
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (fileSystem == null) {
return;
}
@ -285,7 +285,7 @@ public final class OpenFileSystemModel {
}
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (fileSystem == null) {
return;
}
@ -307,7 +307,7 @@ public final class OpenFileSystemModel {
}
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (fileSystem == null) {
return;
}
@ -329,7 +329,7 @@ public final class OpenFileSystemModel {
}
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (fileSystem == null) {
return;
}
@ -363,7 +363,7 @@ public final class OpenFileSystemModel {
}
public void initFileSystem() throws Exception {
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
var fs = store.createFileSystem();
fs.open();
this.fileSystem = fs;
@ -392,7 +392,7 @@ public final class OpenFileSystemModel {
return;
}
BusyProperty.execute(busy, () -> {
BooleanScope.execute(busy, () -> {
if (store instanceof ShellStore s) {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var command = s.control()

View file

@ -4,7 +4,7 @@ import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.Button;
@ -21,7 +21,7 @@ public interface LeafAction extends BrowserAction {
var b = new Button();
b.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(model.getBusy(), () -> {
BooleanScope.execute(model.getBusy(), () -> {
execute(model, selected);
});
});
@ -50,7 +50,7 @@ public interface LeafAction extends BrowserAction {
var mi = new MenuItem(nameFunc.apply(getName(model, selected)));
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(model.getBusy(), () -> {
BooleanScope.execute(model.getBusy(), () -> {
execute(model, selected);
});
});

View file

@ -1,23 +1,24 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleStringProperty;
public class BrowserIcons {
public static PrettyImageComp createDefaultFileIcon() {
return new PrettyImageComp(new SimpleStringProperty("default_file.svg"), 22, 22);
public static Comp<?> createDefaultFileIcon() {
return PrettyImageHelper.ofFixedSquare("default_file.svg", 22);
}
public static PrettyImageComp createDefaultDirectoryIcon() {
return new PrettyImageComp(new SimpleStringProperty("default_folder.svg"), 22, 22);
public static Comp<?> createDefaultDirectoryIcon() {
return PrettyImageHelper.ofFixedSquare("default_folder.svg", 22);
}
public static PrettyImageComp createIcon(FileType type) {
return new PrettyImageComp(new SimpleStringProperty(type.getIcon()), 22, 22);
public static Comp<?> createIcon(FileType type) {
return PrettyImageHelper.ofFixedSquare(type.getIcon(), 22);
}
public static PrettyImageComp createIcon(FileSystem.FileEntry entry) {
return new PrettyImageComp(new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)), 22, 22);
public static Comp<?> createIcon(FileSystem.FileEntry entry) {
return PrettyImageHelper.ofFixedSquare(FileIconManager.getFileIcon(entry, false), 22);
}
}

View file

@ -2,40 +2,13 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.impl.SvgCache;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.scene.image.Image;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class FileIconManager {
@Getter
private static final SvgCache svgCache = createCache();
private static boolean loaded;
private static SvgCache createCache() {
return new SvgCache() {
private final Map<String, Image> images = new HashMap<>();
@Override
public synchronized void put(String image, Image value) {
images.put(image, value);
}
@Override
public synchronized Optional<Image> getCached(String image) {
return Optional.ofNullable(images.get(image));
}
};
}
public static synchronized void loadIfNecessary() {
if (!loaded) {
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons");

View file

@ -1,7 +1,9 @@
package io.xpipe.app.comp;
import io.xpipe.app.comp.base.BackgroundImageComp;
import io.xpipe.app.comp.base.SideMenuBarComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
@ -10,17 +12,17 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.HashMap;
import java.util.Map;
public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
public class AppLayoutComp extends Comp<CompStructure<StackPane>> {
private final AppLayoutModel model = AppLayoutModel.get();
@Override
public CompStructure<BorderPane> createBase() {
public CompStructure<StackPane> createBase() {
var map = new HashMap<AppLayoutModel.Entry, Region>();
getRegion(model.getEntries().get(0), map);
getRegion(model.getEntries().get(1), map);
@ -39,7 +41,12 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
});
});
AppFont.normal(pane);
return new SimpleCompStructure<>(pane);
var bg = new BackgroundImageComp(AppImages.image("bg.png"))
.styleClass("background")
.hide(AppPrefs.get().performanceMode());
return new SimpleCompStructure<>(new StackPane(bg.createRegion(), pane));
}
private Region getRegion(AppLayoutModel.Entry entry, Map<AppLayoutModel.Entry, Region> map) {

View file

@ -33,6 +33,10 @@ public class DeveloperTabComp extends SimpleComp {
System.exit(0);
});
var button6 = new ButtonComp(AppI18n.observable("Restart"), null, () -> {
OperationMode.restart();
});
var button4 = new ButtonComp(AppI18n.observable("Throw terminal exception"), null, () -> {
try {
throw new IllegalStateException();
@ -48,6 +52,7 @@ public class DeveloperTabComp extends SimpleComp {
button2.createRegion(),
button3.createRegion(),
button4.createRegion(),
button5.createRegion());
button5.createRegion(),
button6.createRegion());
}
}

View file

@ -10,14 +10,22 @@ import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import java.util.function.Function;
public class CountComp<T> extends Comp<CompStructure<Label>> {
private final ObservableList<T> sub;
private final ObservableList<T> all;
private final Function<String, String> transformation;
public CountComp(ObservableList<T> sub, ObservableList<T> all) {
this(sub, all, Function.identity());
}
public CountComp(ObservableList<T> sub, ObservableList<T> all, Function<String, String> transformation) {
this.sub = PlatformThread.sync(sub);
this.all = PlatformThread.sync(all);
this.transformation = transformation;
}
@Override
@ -29,9 +37,9 @@ public class CountComp<T> extends Comp<CompStructure<Label>> {
.bind(Bindings.createStringBinding(
() -> {
if (sub.size() == all.size()) {
return all.size() + "";
return transformation.apply(all.size() + "");
} else {
return sub.size() + "/" + all.size();
return transformation.apply(sub.size() + "/" + all.size());
}
},
sub,

View file

@ -0,0 +1,68 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class DropdownComp extends Comp <CompStructure<MenuButton>>{
private final ObservableValue<String> name;
private final ObjectProperty<Node> graphic;
private final List<Comp<?>> items;
public DropdownComp(ObservableValue<String> name, List<Comp<?>> items) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(null);
this.items = items;
}
public DropdownComp(ObservableValue<String> name, Node graphic, List<Comp<?>> items) {
this.name = name;
this.graphic = new SimpleObjectProperty<>(graphic);
this.items = items;
}
public Node getGraphic() {
return graphic.get();
}
public ObjectProperty<Node> graphicProperty() {
return graphic;
}
@Override
public CompStructure<MenuButton> createBase() {
var button = new MenuButton(null);
if (name != null) {
button.textProperty().bind(name);
}
var graphic = getGraphic();
if (graphic instanceof FontIcon f) {
SimpleChangeListener.apply(button.fontProperty(), c -> {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
});
}
button.setGraphic(getGraphic());
button.getStyleClass().add("dropdown-comp");
items.forEach(comp -> {
var i = new MenuItem(null,comp.createRegion());
button.getItems().add(i);
});
return new SimpleCompStructure<>(button);
}
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.FailableConsumer;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.input.Dragboard;
@ -10,7 +11,6 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import lombok.Builder;
import lombok.Value;
import org.apache.commons.lang3.function.FailableConsumer;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;

View file

@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.StackPane;
import lombok.Builder;
@ -93,7 +94,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
@Builder
public static class Structure implements CompStructure<StackPane> {
StackPane pane;
JFXTextField textField;
TextField textField;
@Override
public StackPane get() {

View file

@ -5,6 +5,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@ -29,6 +30,7 @@ public class LoadingOverlayComp extends Comp<CompStructure<StackPane>> {
var loading = new RingProgressIndicator(0, false);
loading.setProgress(-1);
loading.visibleProperty().bind(Bindings.not(AppPrefs.get().performanceMode()));
var loadingOverlay = new StackPane(loading);
loadingOverlay.getStyleClass().add("loading-comp");

View file

@ -1,9 +1,5 @@
package io.xpipe.app.comp.base;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.data.MutableDataSet;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
@ -12,6 +8,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.MarkdownHelper;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@ -44,13 +41,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
}
private String getHtml() {
MutableDataSet options = new MutableDataSet();
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
Document document = parser.parse(markdown.getValue());
var html = renderer.render(document);
var result = htmlTransformation.apply(html);
return "<article class=\"markdown-body\">" + result + "</article>";
return MarkdownHelper.toHtml(markdown.getValue(), htmlTransformation);
}
@SneakyThrows

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.impl.FileNames;
@ -31,7 +31,7 @@ public class OsLogoComp extends SimpleComp {
? getImage(wrapper.getInformation().get()) : null;
},
wrapper.getState(), wrapper.getInformation());
return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), new PrettyImageComp(img, 24, 24))).createRegion();
return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), PrettyImageHelper.ofSvg(img, 24, 24))).createRegion();
}
private static final Map<String, String> ICONS = new HashMap<>();
@ -43,7 +43,7 @@ public class OsLogoComp extends SimpleComp {
}
if (ICONS.isEmpty()) {
AppResources.withResource(AppResources.XPIPE_MODULE, "img/os", ModuleLayer.boot(), file -> {
AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> {
try (var list = Files.list(file)) {
list.filter(path -> !path.toString().endsWith(LINUX_DEFAULT)).map(path -> FileNames.getFileName(path.toString())).forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + ".svg";

View file

@ -2,14 +2,18 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.css.PseudoClass;
@ -17,7 +21,6 @@ import javafx.scene.control.Button;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@ -52,7 +55,32 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
});
{
var fi = new FontIcon("mdi2u-update");
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.apply(new FancyTooltipAugment<>("visitGithubRepository"));
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
vbox.getChildren().add(b.createRegion());
}
{
var b = new IconButtonComp(
"mdal-bug_report",
() -> {
var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory());
}
UserReportComp.show(event.build());
})
.apply(new FancyTooltipAugment<>("reportIssue"));
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
vbox.getChildren().add(b.createRegion());
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
.apply(new FancyTooltipAugment<>("updateAvailableTooltip"));
b.apply(struc -> {

View file

@ -1,28 +0,0 @@
package io.xpipe.app.comp.storage;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.core.source.DataSource;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
@Value
@EqualsAndHashCode(callSuper = true)
public class DataStoreTypeComp extends SimpleComp {
DataSource<?> source;
@Override
protected Region createSimple() {
var icon = new FontIcon("mdoal-insert_drive_file");
var sp = new StackPane(icon);
sp.setAlignment(Pos.CENTER);
icon.iconSizeProperty().bind(Bindings.divide(sp.heightProperty(), 1));
sp.getStyleClass().add("data-store-type-comp");
return sp;
}
}

View file

@ -1,56 +0,0 @@
package io.xpipe.app.comp.storage;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Comparator;
public class StorageFilter {
private final StringProperty filter = new SimpleStringProperty("");
public <T extends Filterable> void createFilterBinding(
ObservableList<T> all, ObservableList<T> shown, ObservableValue<Comparator<T>> order) {
all.addListener((ListChangeListener<? super T>) lc -> {
update(all, shown, order.getValue());
});
SimpleChangeListener.apply(filter, n -> {
update(all, shown, order.getValue());
});
order.addListener((observable, oldValue, newValue) -> {
update(all, shown, newValue);
});
}
private <T extends Filterable> void update(ObservableList<T> all, ObservableList<T> shown, Comparator<T> order) {
var updatedShown = new ArrayList<>(shown);
updatedShown.removeIf(e -> !all.contains(e) || !e.shouldShow(filter.get()));
for (var e : all) {
if (!updatedShown.contains(e) && e.shouldShow(filter.get())) {
updatedShown.add(e);
}
}
updatedShown.sort(order);
shown.setAll(updatedShown);
}
public String getFilter() {
return filter.get();
}
public StringProperty filterProperty() {
return filter;
}
public interface Filterable {
boolean shouldShow(String filter);
}
}

View file

@ -1,10 +1,14 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
public class DenseStoreEntryComp extends StoreEntryComp {
@ -16,6 +20,29 @@ public class DenseStoreEntryComp extends StoreEntryComp {
this.showIcon = showIcon;
}
private Label createInformation(GridPane grid) {
var information = new Label();
information.setGraphicTextGap(7);
information.getStyleClass().add("information");
AppFont.header(information);
var state = wrapper.getEntry().getProvider() != null
? wrapper.getEntry().getProvider().stateDisplay(wrapper)
: Comp.empty();
information.setGraphic(state.createRegion());
SimpleChangeListener.apply(grid.hoverProperty(), val -> {
if (val && wrapper.getSummary().get() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) {
information.textProperty().bind(PlatformThread.sync(wrapper.getSummary()));
} else {
information.textProperty().bind(PlatformThread.sync(wrapper.getInformation()));
}
});
return information;
}
protected Region createContent() {
var name = createName().createRegion();
@ -24,7 +51,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
if (showIcon) {
var storeIcon = createIcon(30, 25);
grid.getColumnConstraints().add(new ColumnConstraints(32));
grid.getColumnConstraints().add(new ColumnConstraints(46));
grid.add(storeIcon, 0, 0);
GridPane.setHalignment(storeIcon, HPos.CENTER);
}
@ -33,9 +60,9 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var custom = new ColumnConstraints(0, customSize, customSize);
custom.setHalignment(HPos.RIGHT);
var info = new ColumnConstraints();
info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);
info.setHalignment(HPos.LEFT);
var infoCC = new ColumnConstraints();
infoCC.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);
infoCC.setHalignment(HPos.LEFT);
var nameCC = new ColumnConstraints();
nameCC.setMinWidth(100);
@ -43,8 +70,9 @@ public class DenseStoreEntryComp extends StoreEntryComp {
grid.getColumnConstraints().addAll(nameCC);
grid.addRow(0, name);
grid.addRow(0, createInformation());
grid.getColumnConstraints().addAll(info, custom);
var info = createInformation(grid);
grid.addRow(0, info);
grid.getColumnConstraints().addAll(infoCC, custom);
var cr = content != null ? content.createRegion() : new Region();
var bb = createButtonBar().createRegion();

View file

@ -22,7 +22,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
var storeIcon = createIcon(50, 39);
grid.add(storeIcon, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(50));
grid.getColumnConstraints().add(new ColumnConstraints(66));
grid.add(name, 1, 0);
grid.add(createSummary(), 1, 1);

View file

@ -0,0 +1,145 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
@Getter
public class StoreCategoryWrapper {
private final int depth;
private final Property<String> name;
private final DataStoreCategory category;
private final Property<Instant> lastAccess;
private final Property<StoreSortMode> sortMode;
private final Property<Boolean> share;
private final ObservableList<StoreCategoryWrapper> children;
private final ObservableList<StoreEntryWrapper> containedEntries;
public StoreCategoryWrapper(DataStoreCategory category) {
var d = 0;
DataStoreCategory p = category;
while ((p = DataStorage.get()
.getStoreCategoryIfPresent(p.getParentCategory())
.orElse(null))
!= null) {
d++;
}
depth = d;
this.category = category;
this.name = new SimpleStringProperty();
this.lastAccess = new SimpleObjectProperty<>();
this.sortMode = new SimpleObjectProperty<>();
this.share = new SimpleObjectProperty<>();
this.children = FXCollections.observableArrayList();
this.containedEntries = FXCollections.observableArrayList();
setupListeners();
update();
}
public StoreCategoryWrapper getParent() {
return StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny().orElse(null);
}
public boolean contains(DataStoreEntry entry) {
return entry.getCategoryUuid().equals(category.getUuid())
|| children.stream().anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry));
}
public void select() {
Platform.runLater(() -> {
StoreViewState.get().getActiveCategory().setValue(this);
});
}
public void delete() {
DataStorage.get().deleteStoreCategory(category);
}
private void setupListeners() {
name.addListener((c, o, n) -> {
category.setName(n);
});
category.addListener(() -> PlatformThread.runLaterIfNeeded(() -> {
update();
}));
sortMode.addListener((observable, oldValue, newValue) -> {
category.setSortMode(newValue);
});
share.addListener((observable, oldValue, newValue) -> {
category.setShare(newValue);
DataStoreCategory p = category;
if (newValue) {
while ((p = DataStorage.get()
.getStoreCategoryIfPresent(p.getParentCategory())
.orElse(null))
!= null) {
p.setShare(true);
}
}
});
}
public void update() {
// Avoid reupdating name when changed from the name property!
if (!category.getName().equals(name.getValue())) {
name.setValue(category.getName());
}
lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));
sortMode.setValue(category.getSortMode());
share.setValue(category.isShare());
if (StoreViewState.get() != null) {
containedEntries.setAll(StoreViewState.get().getAllEntries().stream()
.filter(entry -> contains(entry.getEntry()))
.toList());
children.setAll(StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> getCategory()
.getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory()))
.toList());
Optional.ofNullable(getParent())
.ifPresent(storeCategoryWrapper -> {
storeCategoryWrapper.update();
});
}
}
public String getName() {
return name.getValue();
}
public Property<String> nameProperty() {
return name;
}
public Instant getLastAccess() {
return lastAccess.getValue();
}
public Property<Instant> lastAccessProperty() {
return lastAccess;
}
}

View file

@ -1,74 +0,0 @@
package io.xpipe.app.comp.storage.store;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.store.GuiDsStoreCreator;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class StoreCreationBarComp extends SimpleComp {
@Override
protected Region createSimple() {
var newStreamStore = new ButtonComp(
AppI18n.observable("addCommand"), new FontIcon("mdi2c-code-greater-than"), () -> {
GuiDsStoreCreator.showCreation(
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.COMMAND));
})
.styleClass(Styles.FLAT)
.shortcut(new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addCommand"));
var newHostStore = new ButtonComp(AppI18n.observable("addHost"), new FontIcon("mdi2h-home-plus"), () -> {
GuiDsStoreCreator.showCreation(
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.HOST));
})
.styleClass(Styles.FLAT)
.shortcut(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addHost"));
var newShellStore = new ButtonComp(
AppI18n.observable("addShell"), new FontIcon("mdi2t-text-box-multiple"), () -> {
GuiDsStoreCreator.showCreation(
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.SHELL));
})
.styleClass(Styles.FLAT)
.shortcut(new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addShell"));
var newDbStore = new ButtonComp(AppI18n.observable("addDatabase"), new FontIcon("mdi2d-database-plus"), () -> {
GuiDsStoreCreator.showCreation(
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.DATABASE));
})
.styleClass(Styles.FLAT)
.shortcut(new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addDatabase"));
var newTunnelStore = new ButtonComp(AppI18n.observable("addTunnel"), new FontIcon("mdi2v-vector-polyline-plus"), () -> {
GuiDsStoreCreator.showCreation(
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.TUNNEL));
})
.styleClass(Styles.FLAT)
.shortcut(new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN))
.apply(new FancyTooltipAugment<>("addTunnel"));
var box = new VerticalComp(List.of(newHostStore, newShellStore, newStreamStore, newDbStore, newTunnelStore))
.apply(struc -> struc.get().setFillWidth(true));
box.apply(s -> AppFont.medium(s.get()));
var bar = box.createRegion();
bar.getStyleClass().add("bar");
bar.getStyleClass().add("store-creation-bar");
return bar;
}
}

View file

@ -0,0 +1,85 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.store.GuiDsStoreCreator;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.util.ScanAlert;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreCreationMenu {
public static void addButtons(MenuButton menu) {
{
var automatically = new MenuItem();
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
automatically.setOnAction(event -> {
ScanAlert.showAsync(null);
event.consume();
});
menu.getItems().add(automatically);
menu.getItems().add(new SeparatorMenuItem());
}
{
var host = new MenuItem();
host.setGraphic(new FontIcon("mdi2h-home-plus"));
host.textProperty().bind(AppI18n.observable("addHost"));
host.setOnAction(event -> {
GuiDsStoreCreator.showCreation(DataStoreProviders.byName("ssh").orElseThrow(),
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.HOST));
event.consume();
});
menu.getItems().add(host);
}
{
var shell = new MenuItem();
shell.setGraphic(new FontIcon("mdi2t-text-box-multiple"));
shell.textProperty().bind(AppI18n.observable("addShell"));
shell.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.SHELL));
event.consume();
});
menu.getItems().add(shell);
}
{
var cmd = new MenuItem();
cmd.setGraphic(new FontIcon("mdi2c-code-greater-than"));
cmd.textProperty().bind(AppI18n.observable("addCommand"));
cmd.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.COMMAND));
event.consume();
});
menu.getItems().add(cmd);
}
{
var db = new MenuItem();
db.setGraphic(new FontIcon("mdi2d-database-plus"));
db.textProperty().bind(AppI18n.observable("addDatabase"));
db.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.DATABASE));
event.consume();
});
menu.getItems().add(db);
}
{
var tunnel = new MenuItem();
tunnel.setGraphic(new FontIcon("mdi2v-vector-polyline-plus"));
tunnel.textProperty().bind(AppI18n.observable("addTunnel"));
tunnel.setOnAction(event -> {
GuiDsStoreCreator.showCreation(null,
v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.TUNNEL));
event.consume();
});
menu.getItems().add(tunnel);
}
}
}

View file

@ -1,9 +1,9 @@
package io.xpipe.app.comp.storage.store;
import atlantafx.base.theme.Styles;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider;
@ -13,11 +13,14 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.DesktopShortcuts;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
@ -26,20 +29,37 @@ import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.kordamp.ikonli.javafx.FontIcon;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.ArrayList;
public abstract class StoreEntryComp extends SimpleComp {
public static Comp<?> customSection(StoreSection e) {
public static StoreEntryComp create(
StoreEntryWrapper entry, boolean showIcon, Comp<?> content, boolean preferLarge) {
if (!preferLarge) {
return new DenseStoreEntryComp(entry, showIcon, content);
} else {
return new StandardStoreEntryComp(entry, content);
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customDisplay(e);
return prov.customEntryComp(e, topLevel);
} else {
return new StandardStoreEntryComp(e.getWrapper(), null);
}
@ -47,8 +67,10 @@ public abstract class StoreEntryComp extends SimpleComp {
public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed");
public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete");
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = App.getApp().getStage().widthProperty().divide(2.2);
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = App.getApp().getStage().widthProperty().divide(2.2).add(-300);
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.2);
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.2).add(-300);
protected final StoreEntryWrapper wrapper;
protected final Comp<?> content;
@ -61,7 +83,7 @@ public abstract class StoreEntryComp extends SimpleComp {
protected final Region createSimple() {
var r = createContent();
var button = new JFXButton();
var button = new Button();
button.setGraphic(r);
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(r));
button.getStyleClass().add("store-entry-comp");
@ -83,7 +105,10 @@ public abstract class StoreEntryComp extends SimpleComp {
});
new ContextMenuAugment<>(() -> this.createContextMenu()).augment(new SimpleCompStructure<>(button));
var loading = new LoadingOverlayComp(Comp.of(() -> button), wrapper.getValidating());
var loading = new LoadingOverlayComp(
Comp.of(() -> button),
BindingsHelper.persist(
wrapper.getInRefresh().and(wrapper.getObserving().not())));
return loading.createRegion();
}
@ -163,16 +188,22 @@ public abstract class StoreEntryComp extends SimpleComp {
: wrapper.getEntry()
.getProvider()
.getDisplayIconFileName(wrapper.getEntry().getStore());
var imageComp = new PrettyImageComp(new SimpleStringProperty(img), w, h);
var imageComp = PrettyImageHelper.ofFixed(img, w, h);
var storeIcon = imageComp.createRegion();
storeIcon.getStyleClass().add("icon");
if (wrapper.getState().getValue().isUsable()) {
new FancyTooltipAugment<>(new SimpleStringProperty(
wrapper.getEntry().getProvider().getDisplayName()))
wrapper.getEntry().getProvider().getDisplayName()))
.augment(storeIcon);
}
storeIcon.setPadding(new Insets(3, 0, 0, 0));
return storeIcon;
var stack = new StackPane(storeIcon);
stack.setMinHeight(w + 7);
stack.setMinWidth(w + 7);
stack.setMaxHeight(w + 7);
stack.setMaxWidth(w + 7);
stack.getStyleClass().add("icon");
stack.setAlignment(Pos.CENTER);
return stack;
}
protected Comp<?> createButtonBar() {
@ -227,9 +258,9 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Comp<?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdomz-settings");
var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", () -> {});
settingsButton.styleClass("settings");
settingsButton.accessibleText("Settings");
settingsButton.accessibleText("More");
settingsButton.apply(new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(new FancyTooltipAugment<>("more"));
@ -240,22 +271,38 @@ public abstract class StoreEntryComp extends SimpleComp {
var contextMenu = new ContextMenu();
AppFont.normal(contextMenu.getStyleableNode());
var hasSep = false;
for (var p : wrapper.getActionProviders().entrySet()) {
var actionProvider = p.getKey().getDataStoreCallSite();
if (actionProvider.isMajor(wrapper.getEntry().getStore().asNeeded())) {
continue;
}
if (actionProvider.isSystemAction() && !hasSep) {
if (contextMenu.getItems().size() > 0) {
contextMenu.getItems().add(new SeparatorMenuItem());
}
hasSep = true;
}
var name = actionProvider.getName(wrapper.getEntry().getStore().asNeeded());
var icon = actionProvider.getIcon(wrapper.getEntry().getStore().asNeeded());
var item = new MenuItem(null, new FontIcon(icon));
item.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
var action = actionProvider.createAction(
wrapper.getEntry().getStore().asNeeded());
action.execute();
var item = actionProvider.canLinkTo()
? new Menu(null, new FontIcon(icon))
: new MenuItem(null, new FontIcon(icon));
Menu menu = actionProvider.canLinkTo() ? (Menu) item : null;
item.setOnAction(event -> {
if (menu != null && !event.getTarget().equals(menu)) {
return;
}
contextMenu.hide();
ThreadHelper.runFailableAsync(() -> {
var action = actionProvider.createAction(
wrapper.getEntry().getStore().asNeeded());
action.execute();
});
});
});
item.textProperty().bind(name);
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
item.visibleProperty().bind(p.getValue());
@ -263,19 +310,57 @@ public abstract class StoreEntryComp extends SimpleComp {
item.disableProperty().bind(Bindings.not(p.getValue()));
}
contextMenu.getItems().add(item);
if (menu != null) {
var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than"));
var url = "xpipe://action/" + p.getKey().getId() + "/"
+ wrapper.getEntry().getUuid();
sc.textProperty().bind(AppI18n.observable("base.createShortcut"));
sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
DesktopShortcuts.create(url,
wrapper.getName() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().getStore().asNeeded()).getValue() + ")");
});
});
menu.getItems().add(sc);
if (XPipeDistributionType.get().isSupportsUrls()) {
var l = new MenuItem(null, new FontIcon("mdi2c-clipboard-list-outline"));
l.textProperty().bind(AppI18n.observable("base.copyShareLink"));
l.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
var selection = new StringSelection(url);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
AppActionLinkDetector.setLastDetectedAction(url);
clipboard.setContents(selection, selection);
});
});
menu.getItems().add(l);
}
}
}
if (wrapper.getActionProviders().size() > 0) {
if (contextMenu.getItems().size() > 0 && !hasSep) {
contextMenu.getItems().add(new SeparatorMenuItem());
}
if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(AppI18n.get("browse"), new FontIcon("mdi2f-folder-open-outline"));
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
browse.setOnAction(
event -> DesktopHelper.browsePath(wrapper.getEntry().getDirectory()));
contextMenu.getItems().add(browse);
}
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get().getSortedCategories().forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
});
move.getItems().add(m);
});
contextMenu.getItems().add(move);
var refresh = new MenuItem(AppI18n.get("refresh"), new FontIcon("mdal-360"));
refresh.setOnAction(event -> {
DataStorage.get().refreshAsync(wrapper.getEntry(), true);

View file

@ -1,70 +0,0 @@
package io.xpipe.app.comp.storage.store;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class StoreEntryFlatMiniSectionComp extends SimpleComp {
public static final ObservableList<StoreEntryFlatMiniSectionComp> ALL = FXCollections.observableArrayList();
static {
var topLevel = StoreSection.createTopLevel();
// Listen for any entry list change, not only top level changes
StoreViewState.get().getAllEntries().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
ALL.clear();
var depth = 0;
for (StoreSection v : topLevel.getChildren()) {
add(depth, v);
}
});
var depth = 0;
for (StoreSection v : topLevel.getChildren()) {
add(depth, v);
}
}
private static void add(int depth, StoreSection section) {
if (!section.getWrapper().getState().getValue().isUsable()) {
return;
}
if (!section.getWrapper().getEntry().getProvider().shouldShowInSelectionTree()) {
return;
}
ALL.add(new StoreEntryFlatMiniSectionComp(depth, section.getWrapper().getEntry()));
for (StoreSection child : section.getChildren()) {
add(depth + 1, child);
}
}
int depth;
DataStoreEntry entry;
@Override
protected Region createSimple() {
var image = entry.getState() == DataStoreEntry.State.LOAD_FAILED
? "disabled_icon.png"
: entry.getProvider().getDisplayIconFileName(entry.getStore());
var label =
new Label(entry.getName(), new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion());
var spacer = new Spacer(depth * 10, Orientation.HORIZONTAL);
return new HBox(spacer, label);
}
}

View file

@ -17,15 +17,10 @@ import java.util.List;
public class StoreEntryListComp extends SimpleComp {
private Comp<?> createList() {
var topLevel = StoreSection.createTopLevel();
var filtered = BindingsHelper.filteredContentBinding(
topLevel.getChildren(),
StoreViewState.get()
.getFilterString()
.map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s))));
var content = new ListBoxViewComp<>(filtered, topLevel.getChildren(), (StoreSection e) -> {
var custom = StoreSection.customSection(e).hgrow();
return new HorizontalComp(List.of(Comp.spacer(10), custom, Comp.spacer(10))).styleClass("top");
var topLevel = StoreViewState.get().getTopLevelSection();
var content = new ListBoxViewComp<>(topLevel.getShownChildren(), topLevel.getAllChildren(), (StoreSection e) -> {
var custom = StoreSection.customSection(e, true).hgrow();
return new HorizontalComp(List.of(Comp.hspacer(10), custom, Comp.hspacer(10))).styleClass("top");
}).apply(struc -> ((Region) struc.get().getContent()).setPadding(new Insets(10, 0, 10, 0)));
return content.styleClass("store-list-comp");
}
@ -42,14 +37,14 @@ public class StoreEntryListComp extends SimpleComp {
map.put(
createList(),
BindingsHelper.persist(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getShownEntries()))));
Bindings.not(Bindings.isEmpty(StoreViewState.get().getTopLevelSection().getShownChildren()))));
map.put(new StoreIntroComp(), showIntro);
map.put(
new StoreNotFoundComp(),
BindingsHelper.persist(Bindings.and(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
Bindings.isEmpty(StoreViewState.get().getShownEntries()))));
Bindings.isEmpty(StoreViewState.get().getTopLevelSection().getShownChildren()))));
return new MultiContentComp(map).createRegion();
}
}

View file

@ -2,24 +2,36 @@ package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.CountComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.*;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreEntryListHeaderComp extends SimpleComp {
public class StoreEntryListSideComp extends SimpleComp {
private Region createGroupListHeader() {
var label = new Label("Connections");
label.getStyleClass().add("name");
var count = new CountComp<>(
StoreViewState.get().getShownEntries(), StoreViewState.get().getAllEntries());
var shownList = BindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
storeEntryWrapper -> {
return storeEntryWrapper.shouldShow(
StoreViewState.get().getFilterString().getValue());
},
StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList, StoreViewState.get().getAllEntries());
var spacer = new Region();
@ -35,10 +47,10 @@ public class StoreEntryListHeaderComp extends SimpleComp {
var filterProperty = new SimpleStringProperty();
filterProperty.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runAsync(() -> {
StoreViewState.get().getFilter().filterProperty().setValue(newValue);
StoreViewState.get().getFilterString().setValue(newValue);
});
});
var filter = new FilterComp(StoreViewState.get().getFilter().filterProperty());
var filter = new FilterComp(StoreViewState.get().getFilterString());
filter.shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), s -> {
s.getText().requestFocus();
});
@ -49,9 +61,18 @@ public class StoreEntryListHeaderComp extends SimpleComp {
return r;
}
private Region createButtons() {
var menu = new MenuButton(AppI18n.get("addConnections"), new FontIcon("mdi2p-plus-box-outline"));
AppFont.medium(menu);
GrowAugment.create(true, false).augment(menu);
StoreCreationMenu.addButtons(menu);
return menu;
}
@Override
public Region createSimple() {
var bar = new VBox(createGroupListHeader(), createGroupListFilter());
var bar = new VBox(createGroupListHeader(), createGroupListFilter(), createButtons());
bar.setFillWidth(true);
bar.getStyleClass().add("bar");
bar.getStyleClass().add("store-header-bar");
return bar;

View file

@ -1,44 +0,0 @@
package io.xpipe.app.comp.storage.store;
import javafx.collections.ListChangeListener;
import javafx.scene.control.TreeItem;
public class StoreEntryTree {
public static TreeItem<StoreEntryWrapper> createTree() {
var topLevel = StoreSection.createTopLevel();
var root = new TreeItem<StoreEntryWrapper>();
root.setExpanded(true);
// Listen for any entry list change, not only top level changes
StoreViewState.get().getAllEntries().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
root.getChildren().clear();
for (StoreSection v : topLevel.getChildren()) {
add(root, v);
}
});
for (StoreSection v : topLevel.getChildren()) {
add(root, v);
}
return root;
}
private static void add(TreeItem<StoreEntryWrapper> parent, StoreSection section) {
if (!section.getWrapper().getEntry().getState().isUsable()) {
return;
}
if (!section.getWrapper().getEntry().getProvider().shouldShowInSelectionTree()) {
return;
}
var item = new TreeItem<>(section.getWrapper());
item.setExpanded(section.getWrapper().getExpanded().getValue());
parent.getChildren().add(item);
for (StoreSection child : section.getChildren()) {
add(item, child);
}
}
}

View file

@ -1,12 +1,12 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.comp.store.GuiDsStoreCreator;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
@ -16,17 +16,19 @@ import lombok.Getter;
import java.time.Duration;
import java.time.Instant;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
@Getter
public class StoreEntryWrapper implements StorageFilter.Filterable {
public class StoreEntryWrapper {
private final Property<String> name;
private final DataStoreEntry entry;
private final Property<Instant> lastAccess;
private final BooleanProperty disabled = new SimpleBooleanProperty();
private final BooleanProperty validating = new SimpleBooleanProperty();
private final BooleanProperty inRefresh = new SimpleBooleanProperty();
private final BooleanProperty observing = new SimpleBooleanProperty();
private final Property<DataStoreEntry.State> state = new SimpleObjectProperty<>();
private final StringProperty information = new SimpleStringProperty();
private final StringProperty summary = new SimpleStringProperty();
@ -34,6 +36,9 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
private final Property<ActionProvider.DefaultDataStoreCallSite<?>> defaultActionProvider;
private final BooleanProperty deletable = new SimpleBooleanProperty();
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<StoreEntryWrapper> displayParent = new SimpleObjectProperty<>();
private final IntegerProperty depth = new SimpleIntegerProperty();
public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry;
@ -49,6 +54,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
.getApplicableClass()
.isAssignableFrom(entry.getStore().getClass());
})
.sorted(Comparator.comparing(actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction()))
.forEach(dataStoreActionProvider -> {
actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true));
});
@ -57,6 +63,24 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
update();
}
public void moveTo(DataStoreCategory category) {
ThreadHelper.runAsync(() -> {
DataStorage.get().updateCategory(entry, category);
});
}
private StoreEntryWrapper computeDisplayParent() {
if (StoreViewState.get() == null) {
return null;
}
var p = DataStorage.get().getParent(entry, true).orElse(null);
return StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> storeEntryWrapper.getEntry().equals(p))
.findFirst()
.orElse(null);
}
public boolean isInStorage() {
return DataStorage.get().getStoreEntries().contains(entry);
}
@ -66,8 +90,10 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
}
public void delete() {
DataStorage.get().deleteChildren(this.entry, true);
DataStorage.get().deleteStoreEntry(this.entry);
ThreadHelper.runAsync(() -> {
DataStorage.get().deleteChildren(this.entry, true);
DataStorage.get().deleteStoreEntry(this.entry);
});
}
private void setupListeners() {
@ -85,6 +111,12 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
}
public void update() {
// var cat = StoreViewState.get().getCategories().stream()
// .filter(storeCategoryWrapper ->
// Objects.equals(storeCategoryWrapper.getCategory().getUuid(), entry.getCategoryUuid()))
// .findFirst();
// category.setValue(cat.orElseThrow());
// Avoid reupdating name when changed from the name property!
if (!entry.getName().equals(name.getValue())) {
name.setValue(entry.getName());
@ -94,9 +126,11 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
disabled.setValue(entry.isDisabled());
state.setValue(entry.getState());
expanded.setValue(entry.isExpanded());
observing.setValue(entry.isObserving());
information.setValue(entry.getInformation());
displayParent.setValue(computeDisplayParent());
validating.setValue(entry.isValidating());
inRefresh.setValue(entry.isInRefresh());
if (entry.getState().isUsable()) {
try {
summary.setValue(entry.getProvider().toSummaryString(entry.getStore(), 50));
@ -108,6 +142,13 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
var d = 0;
var c = this;
while ((c = c.getDisplayParent().getValue()) != null) {
d++;
}
depth.setValue(d);
actionProviders.keySet().forEach(dataStoreActionProvider -> {
if (!isInStorage()) {
actionProviders.get(dataStoreActionProvider).set(false);
@ -205,9 +246,9 @@ public class StoreEntryWrapper implements StorageFilter.Filterable {
this.expanded.set(!expanded.getValue());
}
@Override
public boolean shouldShow(String filter) {
return filter == null || getName().toLowerCase().contains(filter.toLowerCase())
return filter == null
|| getName().toLowerCase().contains(filter.toLowerCase())
|| (summary.get() != null && summary.get().toLowerCase().contains(filter.toLowerCase()))
|| (information.get() != null && information.get().toLowerCase().contains(filter.toLowerCase()));
}

View file

@ -3,16 +3,19 @@ package io.xpipe.app.comp.storage.store;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ScanAlert;
import io.xpipe.core.impl.LocalStore;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@ -29,7 +32,7 @@ public class StoreIntroComp extends SimpleComp {
var introDesc = new Label(AppI18n.get("storeIntroDescription"));
var mfi = new FontIcon("mdi2p-playlist-plus");
var machine = new Label(AppI18n.get("storeMachineDescription"), mfi);
var machine = new Label(AppI18n.get("storeMachineDescription"));
machine.heightProperty().addListener((c, o, n) -> {
mfi.iconSizeProperty().set(n.intValue());
});
@ -51,8 +54,15 @@ public class StoreIntroComp extends SimpleComp {
var docLinkPane = new StackPane(docLink);
docLinkPane.setAlignment(Pos.CENTER);
var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 180).createRegion();
var hbox = new HBox(img, new VBox(
title, introDesc, new Separator(Orientation.HORIZONTAL), machine
));
hbox.setSpacing(35);
hbox.setAlignment(Pos.CENTER);
var v = new VBox(
title, introDesc, new Separator(Orientation.HORIZONTAL), machine, scanPane
hbox, scanPane
// new Separator(Orientation.HORIZONTAL),
// documentation,
// docLinkPane

View file

@ -1,6 +1,5 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
@ -8,84 +7,164 @@ import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Value;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
@Value
public class StoreSection implements StorageFilter.Filterable {
public class StoreSection {
public static Comp<?> customSection(StoreSection e) {
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customContainer(e);
return prov.customSectionComp(e, topLevel);
} else {
return new StoreSectionComp(e);
return new StoreSectionComp(e, topLevel);
}
}
StoreEntryWrapper wrapper;
ObservableList<StoreSection> children;
ObservableList<StoreSection> allChildren;
ObservableList<StoreSection> shownChildren;
ObservableList<StoreEntryWrapper> shownEntries;
int depth;
ObservableBooleanValue showDetails;
static ObservableValue<Comparator<StoreSection>> sortMode = Bindings.createObjectBinding(() -> {
return Comparator.<StoreSection>comparingInt(value -> value.getWrapper().getEntry().getState().isUsable() ? 1 : -1)
.thenComparing(StoreViewState.get().getSortMode().getValue().comparator());
}, StoreViewState.get().getSortMode());
public StoreSection(StoreEntryWrapper wrapper, ObservableList<StoreSection> children, int depth) {
public StoreSection(
StoreEntryWrapper wrapper,
ObservableList<StoreSection> allChildren,
ObservableList<StoreSection> shownChildren,
int depth) {
this.wrapper = wrapper;
this.children = children;
this.allChildren = allChildren;
this.shownChildren = shownChildren;
this.depth = depth;
if (wrapper != null) {
this.showDetails = Bindings.createBooleanBinding(
() -> {
return wrapper.getExpanded().get() || children.size() == 0;
return wrapper.getExpanded().get() || allChildren.size() == 0;
},
wrapper.getExpanded(),
children);
allChildren);
} else {
this.showDetails = new SimpleBooleanProperty(true);
}
}
public static StoreSection createTopLevel() {
var topLevel = BindingsHelper.cachedMappedContentBinding(
StoreViewState.get().getAllEntries(), storeEntryWrapper -> create(storeEntryWrapper, 1));
var filtered = BindingsHelper.filteredContentBinding(topLevel, section -> {
return DataStorage.get()
.getParent(section.getWrapper().getEntry(), true)
.isEmpty();
this.shownEntries = FXCollections.observableArrayList();
this.shownChildren.addListener((ListChangeListener<? super StoreSection>) c -> {
shownEntries.clear();
addShown(shownEntries);
});
var ordered = BindingsHelper.orderedContentBinding(filtered, sortMode);
return new StoreSection(null, ordered, 0);
}
private static StoreSection create(StoreEntryWrapper e, int depth) {
private void addShown(List<StoreEntryWrapper> list) {
getShownChildren().forEach(shown -> {
list.add(shown.getWrapper());
shown.addShown(list);
});
}
private static ObservableList<StoreSection> sorted(
ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getState().isUsable() ? 1 : -1);
category.getValue().getSortMode().addListener((observable, oldValue, newValue) -> {
int a = 0;
});
var mapped = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper.getSortMode());
mapped.addListener((observable, oldValue, newValue) -> {
int a = 0;
});
return BindingsHelper.orderedContentBinding(
list,
(o1, o2) -> {
var current = category.getValue();
if (current != null) {
return c.thenComparing(current.getSortMode().getValue().comparator())
.compare(o1, o2);
} else {
return c.compare(o1, o2);
}
},
category,
mapped);
}
public static StoreSection createTopLevel(
ObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) {
var cached = BindingsHelper.cachedMappedContentBinding(
all, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category);
var topLevel = BindingsHelper.filteredContentBinding(
ordered,
section -> {
var noParent = DataStorage.get()
.getParent(section.getWrapper().getEntry(), true)
.isEmpty();
var sameCategory =
category.getValue().contains(section.getWrapper().getEntry());
var diffParentCategory = DataStorage.get()
.getParent(section.getWrapper().getEntry(), true)
.map(entry -> !category.getValue().contains(entry))
.orElse(false);
var showFilter = section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter);
return (noParent || diffParentCategory) && showFilter && sameCategory && matchesSelector;
},
category,
filterString);
return new StoreSection(null, cached, topLevel, 0);
}
private static StoreSection create(
StoreEntryWrapper e,
int depth,
ObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) {
if (e.getEntry().getState() == DataStoreEntry.State.LOAD_FAILED) {
return new StoreSection(e, FXCollections.observableArrayList(), depth);
return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
}
var filtered =
BindingsHelper.filteredContentBinding(StoreViewState.get().getAllEntries(), other -> {
return DataStorage.get()
.getParent(other.getEntry(), true)
.map(found -> found.equals(e.getEntry()))
.orElse(false);
});
var children = BindingsHelper.cachedMappedContentBinding(filtered, entry1 -> create(entry1, depth + 1));
var ordered = BindingsHelper.orderedContentBinding(children, sortMode);
return new StoreSection(e, ordered, depth);
var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
return DataStorage.get()
.getParent(other.getEntry(), true)
.map(found -> found.equals(e.getEntry()))
.orElse(false);
});
var cached = BindingsHelper.cachedMappedContentBinding(
allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category);
var filtered = BindingsHelper.filteredContentBinding(
ordered,
section -> {
return category.getValue().contains(section.getWrapper().getEntry())
&& section.shouldShow(filterString.get())
&& section.anyMatches(entryFilter);
},
category,
filterString);
return new StoreSection(e, cached, filtered, depth);
}
@Override
public boolean shouldShow(String filter) {
return wrapper.shouldShow(filter)
|| children.stream().anyMatch(storeEntrySection -> storeEntrySection.shouldShow(filter));
return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter));
}
public boolean anyMatches(Predicate<StoreEntryWrapper> c) {
return c == null
|| c.test(wrapper)
|| allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));
}
}

View file

@ -24,21 +24,23 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private final StoreSection section;
private final boolean topLevel;
public StoreSectionComp(StoreSection section) {
public StoreSectionComp(StoreSection section, boolean topLevel) {
this.section = section;
this.topLevel = topLevel;
}
@Override
public CompStructure<VBox> createBase() {
var root = StandardStoreEntryComp.customSection(section).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
var root = StandardStoreEntryComp.customSection(section, topLevel).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS));
var button = new IconButtonComp(
Bindings.createStringBinding(
() -> section.getWrapper().getExpanded().get()
&& section.getChildren().size() > 0
&& section.getShownChildren().size() > 0
? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right",
section.getWrapper().getExpanded()),
section.getWrapper().getExpanded(), section.getShownChildren()),
() -> {
section.getWrapper().toggleExpanded();
})
@ -47,24 +49,18 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.focusTraversable()
.accessibleText("Expand")
.disable(BindingsHelper.persist(
Bindings.size(section.getChildren()).isEqualTo(0)))
Bindings.size(section.getShownChildren()).isEqualTo(0)))
.grow(false, true)
.styleClass("expand-button");
List<Comp<?>> topEntryList = List.of(button, root);
var all = section.getChildren();
var shown = BindingsHelper.filteredContentBinding(
all,
StoreViewState.get()
.getFilterString()
.map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s))));
var content = new ListBoxViewComp<>(shown, all, (StoreSection e) -> {
return StoreSection.customSection(e).apply(GrowAugment.create(true, false));
var content = new ListBoxViewComp<>(section.getShownChildren(), section.getAllChildren(), (StoreSection e) -> {
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
}).hgrow();
var expanded = Bindings.createBooleanBinding(() -> {
return section.getWrapper().getExpanded().get() && section.getChildren().size() > 0;
}, section.getWrapper().getExpanded(), section.getChildren());
return section.getWrapper().getExpanded().get() && section.getShownChildren().size() > 0;
}, section.getWrapper().getExpanded(), section.getShownChildren());
return new VerticalComp(List.of(
new HorizontalComp(topEntryList)
@ -75,7 +71,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()),
Bindings.size(section.getChildren()).isEqualTo(0))))))
Bindings.size(section.getAllChildren()).isEqualTo(0))))))
.styleClass("store-entry-section-comp")
.apply(struc -> {
struc.get().setFillWidth(true);

View file

@ -0,0 +1,109 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import lombok.Builder;
import java.util.List;
import java.util.function.BiConsumer;
@Builder
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
public static Comp<?> createList(StoreSection top, BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment) {
var content = new ListBoxViewComp<>(top.getShownChildren(), top.getAllChildren(), (StoreSection e) -> {
var custom = StoreSectionMiniComp.builder().section(e).augment(augment).build().hgrow();
return new HorizontalComp(List.of(custom)).styleClass("top");
});
return content.styleClass("store-mini-list-comp");
}
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private final StoreSection section;
@Builder.Default
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (section1, buttonComp) -> {};
@Override
public CompStructure<VBox> createBase() {
var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {})
.apply(struc -> struc.get()
.setGraphic(PrettyImageHelper.ofFixedSmallSquare(section.getWrapper()
.getEntry()
.getProvider()
.getDisplayIconFileName(section.getWrapper()
.getEntry()
.getStore()))
.createRegion()))
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER_LEFT);
})
.grow(true, false)
.styleClass("item");
augment.accept(section, root);
var expanded = new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
&& section.getAllChildren().size() > 0);
var button = new IconButtonComp(
Bindings.createStringBinding(
() -> expanded.get()
? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right",
expanded),
() -> {
expanded.set(!expanded.get());
})
.apply(struc -> struc.get().setMinWidth(20))
.apply(struc -> struc.get().setPrefWidth(20))
.focusTraversable()
.accessibleText("Expand")
.disable(BindingsHelper.persist(
Bindings.size(section.getAllChildren()).isEqualTo(0)))
.grow(false, true)
.styleClass("expand-button");
List<Comp<?>> topEntryList = List.of(button, root);
var content = new ListBoxViewComp<>(section.getShownChildren(), section.getAllChildren(), (StoreSection e) -> {
return StoreSectionMiniComp.builder().section(e).augment(this.augment).build();
})
.hgrow();
return new VerticalComp(List.of(
new HorizontalComp(topEntryList)
.apply(struc -> struc.get().setFillHeight(true)),
Comp.separator().visible(expanded),
new HorizontalComp(List.of(content))
.styleClass("content")
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or(
Bindings.not(expanded),
Bindings.size(section.getAllChildren()).isEqualTo(0))))))
.styleClass("store-section-mini-comp")
.apply(struc -> {
struc.get().setFillWidth(true);
SimpleChangeListener.apply(expanded, val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val);
});
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
})
.createStructure();
}
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.comp.storage.store;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.util.FeatureProvider;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
@ -14,13 +15,14 @@ public class StoreSidebarComp extends SimpleComp {
@Override
protected Region createSimple() {
var sideBar = new VerticalComp(List.of(
new StoreEntryListHeaderComp(),
new StoreScanBarComp(),
new StoreCreationBarComp(),
new StoreOrganizationComp(),
new StoreEntryListSideComp(),
new StoreSortComp(),
FeatureProvider.get().organizationComp(),
Comp.of(() -> new Region()).styleClass("bar").styleClass("filler-bar")));
sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(4), Priority.ALWAYS));
sideBar.apply(struc -> struc.get().setFillWidth(true));
sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(2), Priority.ALWAYS));
sideBar.styleClass("sidebar");
sideBar.prefWidth(240);
return sideBar.createRegion();
}
}

View file

@ -5,8 +5,10 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
@ -14,12 +16,16 @@ import javafx.scene.layout.Region;
import java.util.List;
public class StoreOrganizationComp extends SimpleComp {
public class StoreSortComp extends SimpleComp {
private final Property<StoreSortMode> sortMode;
public StoreOrganizationComp() {
this.sortMode = StoreViewState.get().getSortMode();
public StoreSortComp() {
this.sortMode = new SimpleObjectProperty<>();
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> {
sortMode.unbind();
sortMode.bindBidirectional(val.getSortMode());
});
}
private Comp<?> createAlphabeticalSortButton() {
@ -102,11 +108,15 @@ public class StoreOrganizationComp extends SimpleComp {
}
private Comp<?> createSortButtonBar() {
return new HorizontalComp(List.of(createDateSortButton(), createAlphabeticalSortButton()));
return new HorizontalComp(List.of(createDateSortButton(), createAlphabeticalSortButton())).apply(struc -> {
struc.get().setMinHeight(40);
struc.get().setPrefHeight(40);
struc.get().setMaxHeight(40);
}).styleClass("bar").styleClass("store-sort-bar");
}
@Override
protected Region createSimple() {
return createSortButtonBar().styleClass("bar").prefHeight(40).createRegion();
return createSortButtonBar().createRegion();
}
}

View file

@ -75,7 +75,7 @@ public interface StoreSortMode {
static Stream<DataStoreEntry> flatten(StoreSection section) {
return Stream.concat(
Stream.of(section.getWrapper().getEntry()),
section.getChildren().stream().flatMap(section1 -> flatten(section1)));
section.getAllChildren().stream().flatMap(section1 -> flatten(section1)));
}
static List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);

View file

@ -1,22 +1,23 @@
package io.xpipe.app.comp.storage.store;
import io.xpipe.app.comp.storage.StorageFilter;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.StorageListener;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.time.Instant;
import java.util.Arrays;
import java.util.Comparator;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@ -24,38 +25,80 @@ public class StoreViewState {
private static StoreViewState INSTANCE;
private final StorageFilter filter = new StorageFilter();
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final ObservableList<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final ObservableList<StoreEntryWrapper> shownEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final Property<StoreSortMode> sortMode;
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final StoreSection topLevelSection;
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
private StoreViewState() {
var val = AppCache.getIfPresent("sortMode", String.class)
.flatMap(StoreSortMode::fromId)
.orElse(StoreSortMode.DATE_ASC);
this.sortMode = new SimpleObjectProperty<>(val);
this.sortMode.addListener((observable, oldValue, newValue) -> {
AppCache.update("sortMode", newValue.getId());
});
StoreSection tl;
try {
addStorageGroupListeners();
addShownContentChangeListeners();
initContent();
addStorageListeners();
tl = StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory);
} catch (Exception exception) {
tl = new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0);
categories.setAll(new StoreCategoryWrapper(DataStorage.get().getAllCategory()));
activeCategory.setValue(getAllCategory());
ErrorEvent.fromThrowable(exception).handle();
}
topLevelSection = tl;
}
public ObservableList<StoreCategoryWrapper> getSortedCategories() {
Comparator<StoreCategoryWrapper> comparator = new Comparator<>() {
@Override
public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) {
if (o1.getParent() == null && o2.getParent() == null) {
return 0;
}
if (o1.getParent() == null) {
return -1;
}
if (o2.getParent() == null) {
return 1;
}
var parent = compare(o1.getParent(), o2.getParent());
if (parent != 0) {
return parent;
}
return o1.getName().compareToIgnoreCase(o2.getName());
}
};
return categories.sorted(comparator);
}
public StoreCategoryWrapper getAllCategory() {
return categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CATEGORY_UUID))
.findFirst()
.orElseThrow();
}
public static void init() {
INSTANCE = new StoreViewState();
new StoreViewState();
}
public static void reset() {
AppCache.update(
"selectedCategory",
INSTANCE.activeCategory.getValue().getCategory().getUuid());
INSTANCE = null;
}
@ -63,17 +106,49 @@ public class StoreViewState {
return INSTANCE;
}
private void addStorageGroupListeners() {
private void initContent() {
allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
.map(StoreEntryWrapper::new)
.toList()));
categories.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
.map(StoreCategoryWrapper::new)
.toList()));
activeCategory.addListener((observable, oldValue, newValue) -> {
DataStorage.get().setSelectedCategory(newValue.getCategory());
});
var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
activeCategory.setValue(categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(selected))
.findFirst()
.orElse(categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID))
.findFirst()
.orElseThrow()));
INSTANCE = this;
categories.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
allEntries.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
}
private void addStorageListeners() {
DataStorage.get().addListener(new StorageListener() {
@Override
public void onStoreAdd(DataStoreEntry... entry) {
var l = Arrays.stream(entry).map(StoreEntryWrapper::new).toList();
Platform.runLater(() -> {
allEntries.addAll(l);
categories.stream()
.filter(storeCategoryWrapper -> allEntries.stream()
.anyMatch(storeEntryWrapper -> storeEntryWrapper
.getEntry()
.getCategoryUuid()
.equals(storeCategoryWrapper
.getCategory()
.getUuid())))
.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
});
}
@ -83,35 +158,52 @@ public class StoreViewState {
var l = StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry()))
.toList();
var cats = categories.stream()
.filter(storeCategoryWrapper -> allEntries.stream()
.anyMatch(storeEntryWrapper -> storeEntryWrapper
.getEntry()
.getCategoryUuid()
.equals(storeCategoryWrapper
.getCategory()
.getUuid()))).toList();
Platform.runLater(() -> {
allEntries.removeAll(l);
cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
});
}
@Override
public void onCategoryAdd(DataStoreCategory category) {
var l = new StoreCategoryWrapper(category);
Platform.runLater(() -> {
categories.add(l);
l.update();
});
}
@Override
public void onCategoryRemove(DataStoreCategory category) {
var found = categories.stream()
.filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().equals(category))
.findFirst();
if (found.isEmpty()) {
return;
}
Platform.runLater(() -> {
categories.remove(found.get());
var p = found.get().getParent();
if (p != null) {
p.update();
}
});
}
});
}
private void addShownContentChangeListeners() {
filter.createFilterBinding(
allEntries,
shownEntries,
new SimpleObjectProperty<>(Comparator.<StoreEntryWrapper, Instant>comparing(
storeEntryWrapper -> storeEntryWrapper.getLastAccess())
.reversed()));
}
public StorageFilter getFilter() {
public Property<String> getFilterString() {
return filter;
}
public ObservableValue<String> getFilterString() {
return filter.filterProperty();
}
public ObservableList<StoreEntryWrapper> getAllEntries() {
return allEntries;
}
public ObservableList<StoreEntryWrapper> getShownEntries() {
return shownEntries;
}
}

View file

@ -1,73 +0,0 @@
package io.xpipe.app.comp.store;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.Property;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class DataStoreSelectorComp extends Comp<CompStructure<Button>> {
DataStoreProvider.DisplayCategory category;
Property<DataStore> chosenStore;
@Override
public CompStructure<Button> createBase() {
var button = new JFXButton();
button.setGraphic(getGraphic());
button.setOnAction(e -> {
GuiDsStoreCreator.show(
"inProgress",
null,
null,
v -> v.getDisplayCategory().equals(category),
entry -> {
chosenStore.setValue(entry.getStore());
},
false);
e.consume();
});
Runnable update = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newGraphic = getGraphic();
button.setGraphic(newGraphic);
button.layout();
});
};
chosenStore.addListener((c, o, n) -> {
update.run();
});
return new SimpleCompStructure<>(button);
}
private Region getGraphic() {
var provider = chosenStore.getValue() != null
? DataStoreProviders.byStoreClass(chosenStore.getValue().getClass())
.orElse(null)
: null;
var graphic = provider != null ? provider.getDisplayIconFileName(chosenStore.getValue()) : "file_icon.png";
if (chosenStore.getValue() == null || !(chosenStore.getValue() instanceof FileStore f)) {
return JfxHelper.createNamedEntry(
AppI18n.get("selectStreamStore"), AppI18n.get("openStreamStoreWizard"), graphic);
} else {
return JfxHelper.createNamedEntry(f.getFileName(), f.getPath(), graphic);
}
}
}

View file

@ -1,61 +0,0 @@
package io.xpipe.app.comp.store;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.impl.FileStore;
import javafx.beans.property.Property;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.apache.commons.io.FilenameUtils;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class DsFileHistoryComp extends SimpleComp {
private final DataSourceProvider<?> provider;
private final Property<FileStore> file;
public DsFileHistoryComp(DataSourceProvider<?> provider, Property<FileStore> file) {
this.provider = provider;
this.file = file;
}
@Override
public Region createSimple() {
var previous = new VBox();
List<String> cached = AppCache.get("csv-data-sources", List.class, ArrayList::new);
if (cached.size() == 0) {
return previous;
}
previous.setFillWidth(true);
var label = new Label(AppI18n.get("recentFiles"));
AppFont.header(label);
previous.getChildren().add(label);
cached.forEach(s -> {
var graphic = provider.getDisplayIconFileName();
var el = JfxHelper.createNamedEntry(FilenameUtils.getName(s), s, graphic);
var b = new JFXButton();
b.setGraphic(el);
b.prefWidthProperty().bind(previous.widthProperty());
b.setOnAction(e -> {
file.setValue(FileStore.local(Path.of(s)));
});
previous.getChildren().add(b);
});
var pane = new ScrollPane(previous);
pane.setFitToWidth(true);
return previous;
}
}

View file

@ -1,67 +0,0 @@
package io.xpipe.app.comp.store;
import com.jfoenix.controls.JFXButton;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.JfxHelper;
import javafx.beans.property.Property;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import javafx.stage.DirectoryChooser;
import java.io.File;
import java.nio.file.Path;
public class DsLocalDirectoryBrowseComp extends Comp<CompStructure<Button>> {
private final DataSourceProvider<?> provider;
private final Property<Path> chosenDir;
public DsLocalDirectoryBrowseComp(DataSourceProvider<?> provider, Property<Path> chosenDir) {
this.provider = provider;
this.chosenDir = chosenDir;
}
@Override
public CompStructure<Button> createBase() {
var button = new JFXButton();
button.setGraphic(getGraphic());
button.setOnAction(e -> {
var dirChooser = new DirectoryChooser();
dirChooser.setTitle(AppI18n.get(
"browseDirectoryTitle", provider.getFileProvider().getFileName()));
File file = dirChooser.showDialog(button.getScene().getWindow());
if (file != null && file.exists()) {
chosenDir.setValue(file.toPath());
}
e.consume();
});
chosenDir.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
var newGraphic = getGraphic();
button.setGraphic(newGraphic);
button.layout();
});
});
return new SimpleCompStructure<>(button);
}
private Region getGraphic() {
var graphic = provider.getDisplayIconFileName();
if (chosenDir.getValue() == null) {
return JfxHelper.createNamedEntry(
AppI18n.get("browse"), AppI18n.get("selectDirectoryFromComputer"), graphic);
} else {
return JfxHelper.createNamedEntry(
chosenDir.getValue().getFileName().toString(),
chosenDir.getValue().toString(),
graphic);
}
}
}

View file

@ -1,77 +0,0 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.browser.StandaloneFileBrowser;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.impl.LocalStore;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import java.util.concurrent.atomic.AtomicReference;
@AllArgsConstructor
public class DsLocalFileBrowseComp extends Comp<CompStructure<Button>> {
private final ObservableValue<DataSourceProvider<?>> provider;
private final Property<FileStore> chosenFile;
private final DsStreamStoreChoiceComp.Mode mode;
@Override
public CompStructure<Button> createBase() {
var button = new AtomicReference<Button>();
button.set(new ButtonComp(null, getGraphic(), () -> {
if (mode == DsStreamStoreChoiceComp.Mode.OPEN) {
StandaloneFileBrowser.openSingleFile(() -> new LocalStore(), fileStore -> {
chosenFile.setValue(fileStore);
});
} else {
StandaloneFileBrowser.saveSingleFile(chosenFile);
}
})
.createStructure()
.get());
Runnable update = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newGraphic = getGraphic();
button.get().setGraphic(newGraphic);
button.get().layout();
});
};
chosenFile.addListener((c, o, n) -> {
update.run();
});
if (provider != null) {
provider.addListener((c, o, n) -> {
update.run();
});
}
return new SimpleCompStructure<>(button.get());
}
private boolean hasProvider() {
return provider != null && provider.getValue() != null;
}
private Region getGraphic() {
var graphic = hasProvider() ? provider.getValue().getDisplayIconFileName() : "file_icon.png";
if (chosenFile.getValue() == null || !(chosenFile.getValue() instanceof FileStore f) || f.getPath() == null) {
return JfxHelper.createNamedEntry(AppI18n.get("browse"), AppI18n.get("selectFileFromComputer"), graphic);
} else {
return JfxHelper.createNamedEntry(f.getFileName(), f.getPath(), graphic);
}
}
}

View file

@ -1,42 +0,0 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.util.DynamicOptionsBuilder;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class DsRemoteFileChoiceComp extends SimpleComp {
Property<DataStore> store;
@Override
protected Region createSimple() {
var machine = new SimpleObjectProperty<FileSystemStore>();
var fileName = new SimpleStringProperty();
return new DynamicOptionsBuilder(false)
.addString(AppI18n.observable("file"), fileName, true)
.bind(
() -> {
if (fileName.get() == null || machine.get() == null) {
return null;
}
return FileStore.builder()
.fileSystem(machine.get())
.path(fileName.get())
.build();
},
store)
.build();
}
}

View file

@ -50,9 +50,13 @@ public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName());
getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.canManuallyCreate() || staticDisplay)
var l = getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.canManuallyCreate() || staticDisplay).toList();
l
.forEach(comboBox::add);
if (l.size() == 1) {
provider.setValue(l.get(0));
}
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("data-source-type");
cb.getStyleClass().add("choice-comp");

View file

@ -1,187 +0,0 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.FileDropOverlayComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.ext.DataSourceProviders;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.TabPaneComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.SimpleValidator;
import io.xpipe.app.util.Validatable;
import io.xpipe.app.util.Validator;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.synedra.validatorfx.Check;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Value
@EqualsAndHashCode(callSuper = true)
public class DsStreamStoreChoiceComp extends SimpleComp implements Validatable {
public enum Mode {
OPEN,
WRITE
}
Property<DataStore> selected;
Property<DataSourceProvider<?>> provider;
boolean showAnonymous;
boolean showSaved;
Validator validator;
Check check;
DsStreamStoreChoiceComp.Mode mode;
public DsStreamStoreChoiceComp(
Property<DataStore> selected,
Property<DataSourceProvider<?>> provider,
boolean showAnonymous,
boolean showSaved,
Mode mode) {
this.selected = selected;
this.provider = provider;
this.showAnonymous = showAnonymous;
this.showSaved = showSaved;
this.mode = mode;
validator = new SimpleValidator();
check = Validator.nonNull(validator, AppI18n.observable("streamStore"), selected);
}
@Override
protected Region createSimple() {
var isNamedStore =
DataStorage.get().getStoreDisplayName(selected.getValue()).isPresent();
var localStore = new SimpleObjectProperty<>(
!isNamedStore
&& selected.getValue() instanceof FileStore fileStore
&& fileStore.getFileSystem() instanceof LocalStore
? fileStore
: null);
var browseComp = new DsLocalFileBrowseComp(provider, localStore, mode).apply(GrowAugment.create(true, false));
var dragAndDropLabel = Comp.of(() -> new Label(AppI18n.get("dragAndDropFilesHere")))
.apply(s -> s.get().setAlignment(Pos.CENTER))
.apply(struc -> AppFont.small(struc.get()));
// var historyComp = new DsFileHistoryComp(provider, chosenFile);
var local = new TabPaneComp.Entry(
AppI18n.observable("localFile"),
"mdi2m-monitor",
new VerticalComp(List.of(browseComp, dragAndDropLabel))
.styleClass("store-local-file-chooser")
.apply(s -> s.get().setFillWidth(true))
.apply(s -> s.get().setSpacing(30))
.apply(s -> s.get().setAlignment(Pos.TOP_CENTER)));
var filter = Bindings.createObjectBinding(
() -> (Predicate<DataStoreEntry>) e -> {
if (provider == null || provider.getValue() == null) {
return e.getStore() instanceof StreamDataStore;
}
return provider.getValue().couldSupportStore(e.getStore());
},
provider != null ? provider : new SimpleObjectProperty<>());
var remoteStore = new SimpleObjectProperty<>(
isNamedStore
&& selected.getValue() instanceof FileStore fileStore
&& !(fileStore.getFileSystem() instanceof LocalStore)
? selected.getValue()
: null);
var remote = new TabPaneComp.Entry(
AppI18n.observable("remote"), "mdi2e-earth", new DsRemoteFileChoiceComp(remoteStore));
var namedStore = new SimpleObjectProperty<>(isNamedStore ? selected.getValue() : null);
var named = new TabPaneComp.Entry(
AppI18n.observable("stored"),
"mdrmz-storage",
NamedStoreChoiceComp.create(filter, namedStore, DataStoreProvider.DataCategory.STREAM));
var otherStore = new SimpleObjectProperty<>(
localStore.get() == null && remoteStore.get() == null && !isNamedStore ? selected.getValue() : null);
var other = new TabPaneComp.Entry(
AppI18n.observable("other"),
"mdrmz-web_asset",
new DataStoreSelectorComp(DataStoreProvider.DisplayCategory.HOST, otherStore));
var selectedTab = new SimpleObjectProperty<TabPaneComp.Entry>();
if (localStore.get() != null) {
selectedTab.set(local);
} else if (remoteStore.get() != null) {
selectedTab.set(remote);
} else if (namedStore.get() != null) {
selectedTab.set(named);
} else if (otherStore.get() != null) {
selectedTab.set(other);
} else {
selectedTab.set(local);
}
selected.addListener((observable, oldValue, newValue) -> {
if (provider != null && provider.getValue() == null) {
provider.setValue(
DataSourceProviders.byPreferredStore(newValue, null).orElse(null));
}
});
SimpleChangeListener.apply(selectedTab, c -> {
if (c == local) {
this.selected.bind(localStore);
}
if (c == remote) {
this.selected.bind(remoteStore);
}
if (c == named) {
this.selected.bind(namedStore);
}
if (c == other) {
this.selected.bind(otherStore);
}
});
var entries = new ArrayList<>(List.of(local, remote));
if (showSaved) {
entries.add(named);
}
if (showAnonymous) {
entries.add(other);
}
var pane = new TabPaneComp(selectedTab, entries);
pane.apply(s -> AppFont.normal(s.get()));
var fileDrop = new FileDropOverlayComp<>(pane, files -> {
if (files.size() != 1) {
return;
}
var f = files.get(0);
var store = FileStore.local(f);
selectedTab.set(local);
localStore.set(store);
});
var region = fileDrop.createRegion();
check.decorates(region);
return region;
}
}

View file

@ -56,6 +56,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
BooleanProperty changedSinceError = new SimpleBooleanProperty();
StringProperty name;
boolean exists;
boolean staticDisplay;
public GuiDsStoreCreator(
MultiStepComp parent,
@ -63,13 +64,15 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
Property<DataStore> store,
Predicate<DataStoreProvider> filter,
String initialName,
boolean exists) {
boolean exists, boolean staticDisplay
) {
this.parent = parent;
this.provider = provider;
this.store = store;
this.filter = filter;
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
this.exists = exists;
this.staticDisplay = staticDisplay;
this.store.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
@ -112,14 +115,14 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
}
});
},
true);
true, true);
}
public static void showCreation(Predicate<DataStoreProvider> filter) {
public static void showCreation(DataStoreProvider selected, Predicate<DataStoreProvider> filter) {
show(
null,
null,
null,
selected,
selected != null ? selected.defaultStore() : null,
filter,
e -> {
try {
@ -131,7 +134,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
ErrorEvent.fromThrowable(ex).handle();
}
},
false);
false, false);
}
public static void show(
@ -140,7 +143,8 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
DataStore s,
Predicate<DataStoreProvider> filter,
Consumer<DataStoreEntry> con,
boolean exists) {
boolean exists,
boolean staticDisplay) {
var prop = new SimpleObjectProperty<>(provider);
var store = new SimpleObjectProperty<>(s);
var loading = new SimpleBooleanProperty();
@ -152,7 +156,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
return new MultiStepComp() {
private final GuiDsStoreCreator creator =
new GuiDsStoreCreator(this, prop, store, filter, initialName, exists);
new GuiDsStoreCreator(this, prop, store, filter, initialName, exists, staticDisplay);
@Override
protected List<Entry> setup() {
@ -223,7 +227,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
return null;
}
return DataStoreEntry.createNew(UUID.randomUUID(), name.getValue(), store.getValue());
return DataStoreEntry.createNew(UUID.randomUUID(), DataStorage.get().getSelectedCategory().getUuid(), name.getValue(), store.getValue());
},
entry)
.build();
@ -240,8 +244,8 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
var layout = new BorderPane();
layout.getStyleClass().add("store-creator");
layout.setPadding(new Insets(20));
var providerChoice = new DsStoreProviderChoiceComp(filter, provider, provider.getValue() != null);
if (provider.getValue() != null) {
var providerChoice = new DsStoreProviderChoiceComp(filter, provider, staticDisplay);
if (staticDisplay) {
providerChoice.apply(struc -> struc.get().setDisable(true));
}
providerChoice.apply(GrowAugment.create(true, false));
@ -286,7 +290,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
var install = provider.getValue().getRequiredAdditionalInstallation();
if (install != null && !AppExtensionManager.getInstance().isInstalled(install)) {
ThreadHelper.runAsync(() -> {
try (var ignored = new BusyProperty(busy)) {
try (var ignored = new BooleanScope(busy).start()) {
AppExtensionManager.getInstance().installIfNeeded(install);
/*
TODO: Use reload
@ -344,7 +348,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
}
ThreadHelper.runAsync(() -> {
try (var b = new BusyProperty(busy)) {
try (var b = new BooleanScope(busy).start()) {
entry.getValue().refresh(true);
finished.setValue(true);
PlatformThread.runLaterIfNeeded(parent::next);

View file

@ -1,162 +0,0 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListViewComp;
import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.app.util.SimpleValidator;
import io.xpipe.app.util.Validatable;
import io.xpipe.app.util.Validator;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Getter;
import net.synedra.validatorfx.Check;
import java.util.List;
import java.util.function.Predicate;
public class NamedStoreChoiceComp extends SimpleComp implements Validatable {
private final ObservableValue<Predicate<DataStore>> filter;
private final DataStoreProvider.DataCategory category;
private final Property<? extends DataStore> selected;
private final StringProperty filterString = new SimpleStringProperty();
@Getter
private final Validator validator = new SimpleValidator();
private final Check check;
public NamedStoreChoiceComp(
ObservableValue<Predicate<DataStore>> filter,
Property<? extends DataStore> selected,
DataStoreProvider.DataCategory category) {
this.filter = filter;
this.selected = selected;
this.category = category;
check = Validator.nonNull(validator, AppI18n.observable("store"), selected);
}
public static NamedStoreChoiceComp create(
ObservableValue<Predicate<DataStoreEntry>> filter,
Property<? extends DataStore> selected,
DataStoreProvider.DataCategory category) {
return new NamedStoreChoiceComp(
Bindings.createObjectBinding(
() -> {
return store -> {
if (store == null) {
return false;
}
var e = DataStorage.get().getStoreEntry(store);
return filter.getValue().test(e);
};
},
filter),
selected,
category);
}
private void setUpListener(ObservableValue<DataStoreEntry> prop) {
prop.addListener((c, o, n) -> {
selected.setValue(n != null ? n.getStore().asNeeded() : null);
});
}
private void refreshShown(ObservableList<DataStoreEntry> list, ObservableList<DataStoreEntry> shown) {
var filtered = list.filtered(e -> filter.getValue().test(e.getStore())).filtered(e -> {
return filterString.get() == null || e.matches(filterString.get());
});
shown.removeIf(store -> !filtered.contains(store));
filtered.forEach(store -> {
if (!shown.contains(store)) {
shown.add(store);
}
});
}
@Override
protected Region createSimple() {
var list = FXCollections.<DataStoreEntry>observableArrayList();
BindingsHelper.bindMappedContent(list, StoreViewState.get().getAllEntries(), v -> v.getEntry());
var shown = FXCollections.<DataStoreEntry>observableArrayList();
refreshShown(list, shown);
list.addListener((ListChangeListener<? super DataStoreEntry>) c -> {
refreshShown(list, shown);
});
filter.addListener((observable, oldValue, newValue) -> {
refreshShown(list, shown);
});
filterString.addListener((observable, oldValue, newValue) -> {
refreshShown(list, shown);
});
var prop = new SimpleObjectProperty<>(
selected.getValue() != null
? DataStorage.get()
.getStoreEntryIfPresent(selected.getValue())
.orElse(null)
: null);
setUpListener(prop);
var filterComp = new FilterComp(filterString)
.hide(BindingsHelper.persist(Bindings.greaterThan(5, Bindings.size(shown))));
var view = new ListViewComp<>(shown, list, prop, (DataStoreEntry e) -> {
var provider = e.getProvider();
var graphic = provider.getDisplayIconFileName(e.getStore());
var top = String.format("%s (%s)", e.getName(), provider.getDisplayName());
var bottom = provider.toSummaryString(e.getStore(), 50);
var el = JfxHelper.createNamedEntry(top, bottom, graphic);
VBox.setVgrow(el, Priority.ALWAYS);
return Comp.of(() -> el);
})
.apply(struc -> {
struc.get().setMaxHeight(2000);
check.decorates(struc.get());
});
var box = new VerticalComp(List.of(filterComp, view));
var text = new LabelComp(AppI18n.observable("noMatchingStoreFound"))
.apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS));
var addButton = new ButtonComp(AppI18n.observable("addStore"), null, () -> {
// GuiDsStoreCreator.showCreation(v -> v.getCategory().equals(category));
});
var notice = new VerticalComp(List.of(text, addButton))
.apply(struc -> {
struc.get().setSpacing(10);
struc.get().setAlignment(Pos.CENTER);
})
.hide(BindingsHelper.persist(Bindings.notEqual(0, Bindings.size(shown))));
return new StackComp(List.of(box, notice))
.styleClass("named-store-choice")
.createRegion();
}
}

View file

@ -66,7 +66,7 @@ public class App extends Application {
public void close() {
Platform.runLater(() -> {
stage.hide();
Stage.getWindows().stream().toList().forEach(w -> w.hide());
TrackEvent.debug("Closed main window");
});
}

View file

@ -43,10 +43,9 @@ public class AppAntivirusAlert {
alert.setTitle(AppI18n.get("antivirusNoticeTitle"));
alert.setAlertType(Alert.AlertType.NONE);
AppResources.withResource(
AppResources.with(
AppResources.XPIPE_MODULE,
"misc/antivirus.md",
AppExtensionManager.getInstance().getExtendedLayer(),
file -> {
var markdown = new MarkdownComp(Files.readString(file), s -> s.formatted(found.get(), found.get(), AppProperties.get().getVersion(), AppProperties.get().getVersion(), found.get())).prefWidth(550).prefHeight(600).createRegion();
alert.getDialogPane().setContent(markdown);

View file

@ -23,23 +23,28 @@ import java.util.stream.Stream;
public class AppExtensionManager {
private static AppExtensionManager INSTANCE;
private final boolean loadedProviders;
private final List<Extension> loadedExtensions = new ArrayList<>();
private final List<ModuleLayer> leafModuleLayers = new ArrayList<>();
private final List<Path> extensionBaseDirectories = new ArrayList<>();
private ModuleLayer baseLayer = ModuleLayer.boot();
private ModuleLayer extendedLayer;
public AppExtensionManager(boolean loadedProviders) {
this.loadedProviders = loadedProviders;
}
public static void init(boolean loadProviders) {
if (INSTANCE != null) {
return;
}
var load = INSTANCE == null || !INSTANCE.loadedProviders && loadProviders;
if (INSTANCE == null) {
INSTANCE = new AppExtensionManager(loadProviders);
INSTANCE.determineExtensionDirectories();
INSTANCE.loadBaseExtension();
INSTANCE.loadAllExtensions();
}
INSTANCE = new AppExtensionManager();
INSTANCE.determineExtensionDirectories();
INSTANCE.loadBaseExtension();
INSTANCE.loadAllExtensions();
if (loadProviders) {
if (load) {
INSTANCE.addNativeLibrariesToPath();
XPipeServiceProviders.load(INSTANCE.extendedLayer);
MessageExchangeImpls.loadAll();
@ -65,7 +70,7 @@ public class AppExtensionManager {
}
if (!AppProperties.get().isFullVersion()) {
var localInstallation = XPipeInstallation.getLocalDefaultInstallationBasePath(true);
var localInstallation = XPipeInstallation.getLocalDefaultInstallationBasePath();
Path p = Path.of(localInstallation);
if (!Files.exists(p)) {
throw new IllegalStateException(
@ -258,7 +263,7 @@ public class AppExtensionManager {
return l.modules().stream()
.map(m -> {
AtomicReference<Extension> ext = new AtomicReference<>();
AppResources.withResource(m.getName(), "extension.properties", l, path -> {
AppResources.withResourceInLayer(m.getName(), "extension.properties", l, path -> {
if (Files.exists(path)) {
var props = new Properties();
try (var in = Files.newInputStream(path)) {
@ -288,7 +293,7 @@ public class AppExtensionManager {
}
for (var ext : loadedExtensions) {
AppResources.withResource(ext.getModule().getName(), "lib", extendedLayer, path -> {
AppResources.withResourceInLayer(ext.getModule().getName(), "lib", extendedLayer, path -> {
if (Files.exists(path)) {
Files.list(path).forEach(lib -> {
try {

View file

@ -4,13 +4,18 @@ import com.jfoenix.controls.JFXCheckBox;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.MarkdownHelper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Modality;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.function.UnaryOperator;
@ -33,30 +38,14 @@ public class AppGreetings {
return tp;
}
private static TitledPane createPrivacyPolicy() {
private static TitledPane createTos() {
var tp = new TitledPane();
tp.setExpanded(false);
tp.setText(AppI18n.get("privacyPolicy"));
tp.setText(AppI18n.get("tos"));
tp.setAlignment(Pos.CENTER_LEFT);
AppFont.normal(tp);
AppResources.with(AppResources.XPIPE_MODULE, "misc/privacy.md", file -> {
var md = Files.readString(file);
var markdown = new MarkdownComp(md, UnaryOperator.identity()).createRegion();
tp.setContent(markdown);
});
return tp;
}
private static TitledPane createEULA() {
var tp = new TitledPane();
tp.setExpanded(false);
tp.setText(AppI18n.get("eula"));
tp.setAlignment(Pos.CENTER_LEFT);
AppFont.normal(tp);
AppResources.with(AppResources.XPIPE_MODULE, "misc/eula.md", file -> {
AppResources.with(AppResources.XPIPE_MODULE, "misc/tos.md", file -> {
var md = Files.readString(file);
var markdown = new MarkdownComp(md, UnaryOperator.identity()).createRegion();
tp.setContent(markdown);
@ -66,26 +55,25 @@ public class AppGreetings {
}
public static void showIfNeeded() {
// TODO
//noinspection PointlessBooleanExpression
if (!AppProperties.get().isImage() || true) {
return;
}
boolean set = AppCache.get("legalAccepted", Boolean.class, () -> false);
if (set) {
if (set || !AppState.get().isInitialLaunch()) {
return;
}
var read = new SimpleBooleanProperty();
var accepted = new SimpleBooleanProperty();
AppWindowHelper.showAlert(
alert -> {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("greetingsAlertTitle"));
alert.setAlertType(Alert.AlertType.NONE);
alert.initModality(Modality.APPLICATION_MODAL);
var content = List.of(createIntroduction(), createEULA(), createPrivacyPolicy());
var content = List.of(createIntroduction(), createTos());
var accordion = new Accordion(content.toArray(TitledPane[]::new));
accordion.setExpandedPane(content.get(0));
accordion.expandedPaneProperty().addListener((observable, oldValue, newValue) -> {
if (content.get(1).equals(newValue)) {
read.set(true);
}
});
var acceptanceBox = Comp.of(() -> {
var cb = new JFXCheckBox();
@ -109,17 +97,43 @@ public class AppGreetings {
alert.getDialogPane().setContent(layout);
var buttonType = new ButtonType(AppI18n.get("confirm"), ButtonBar.ButtonData.OK_DONE);
alert.getButtonTypes().add(buttonType);
var button = alert.getDialogPane().lookupButton(buttonType);
button.disableProperty().bind(accepted.not());
},
null,
r -> r.filter(b -> b.getButtonData().isDefaultButton() && accepted.get())
.ifPresentOrElse(
t -> {
AppCache.update("legalAccepted", true);
},
OperationMode::close));
{
var view = new ButtonType(AppI18n.get("print"), ButtonBar.ButtonData.OTHER);
alert.getButtonTypes().add(view);
Button button = (Button) alert.getDialogPane().lookupButton(view);
button.visibleProperty().bind(read);
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
try {
var temp = Files.createTempFile("tos", ".html");
AppResources.with(AppResources.XPIPE_MODULE, "misc/tos.md", file -> {
Files.writeString(
temp,
MarkdownHelper.toHtml(Files.readString(file), UnaryOperator.identity()));
});
App.getApp()
.getHostServices()
.showDocument(temp.toUri().toString());
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
event.consume();
});
}
{
var buttonType = new ButtonType(AppI18n.get("confirm"), ButtonBar.ButtonData.OK_DONE);
alert.getButtonTypes().add(buttonType);
Button button = (Button) alert.getDialogPane().lookupButton(buttonType);
button.disableProperty().bind(BindingsHelper.persist(accepted.not()));
}
alert.getButtonTypes().add(ButtonType.CANCEL);
})
.filter(b -> b.getButtonData().isDefaultButton() && accepted.get())
.ifPresentOrElse(
t -> {
AppCache.update("legalAccepted", true);
},
OperationMode::close);
}
}

View file

@ -80,6 +80,18 @@ public class AppI18n {
instant);
}
public static StringBinding readableDuration(ObservableValue<Instant> instant) {
return Bindings.createStringBinding(
() -> {
if (instant.getValue() == null) {
return "null";
}
return getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1)));
},
instant);
}
public static ObservableValue<String> observable(String s, Object... vars) {
if (s == null) {
return null;

View file

@ -123,7 +123,7 @@ public class AppImages {
return img;
}
private static Image loadImage(Path p) {
public static Image loadImage(Path p) {
if (p == null) {
return DEFAULT_IMAGE;
}

View file

@ -6,6 +6,7 @@ import io.xpipe.app.comp.DeveloperTabComp;
import io.xpipe.app.comp.storage.store.StoreLayoutComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.prefs.PrefsComp;
import io.xpipe.app.util.FeatureProvider;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@ -43,6 +44,10 @@ public class AppLayoutModel {
selected.setValue(entries.get(2));
}
public void selectLicense() {
selected.setValue(entries.get(3));
}
public void selectConnections() {
selected.setValue(entries.get(1));
}
@ -52,7 +57,6 @@ public class AppLayoutModel {
new Entry(
AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)),
new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
new Entry(
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this))));
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
@ -63,13 +67,10 @@ public class AppLayoutModel {
AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
}
// l.add(new SideMenuBarComp.Entry(AppI18n.observable("abc"), "mdi2b-book-open-variant", Comp.of(() -> {
// var fi = new FontIcon("mdsal-dvr");
// fi.setIconSize(30);
// fi.setIconColor(Color.valueOf("#111C"));
// JfxHelper.addEffect(fi);
// return new StackPane(fi);
// })));
l.add(new Entry(
AppI18n.observable("explorePlans"),
"mdi2p-professional-hexagon",
FeatureProvider.get().overviewPage()));
return l;
}

View file

@ -244,6 +244,7 @@ public class AppMainWindow {
var contentR = content.createRegion();
contentR.requestFocus();
stage.getScene().setRoot(contentR);
AppTheme.initTheme(stage);
TrackEvent.debug("Set content scene");
contentR.prefWidthProperty().bind(stage.getScene().widthProperty());
@ -262,6 +263,7 @@ public class AppMainWindow {
if (AppProperties.get().isDeveloperMode() && event.getCode().equals(KeyCode.F6)) {
var newR = content.createRegion();
stage.getScene().setRoot(newR);
AppTheme.initTheme(stage);
newR.requestFocus();
TrackEvent.debug("Rebuilt content");

View file

@ -3,10 +3,10 @@ package io.xpipe.app.core;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
import lombok.Getter;
import lombok.Value;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
@ -16,8 +16,6 @@ import java.util.UUID;
@Value
public class AppProperties {
public static final Path DEFAULT_DATA_DIR = Path.of(System.getProperty("user.home"), ".xpipe");
private static final String DATA_DIR_PROP = "io.xpipe.app.dataDir";
private static final String EXTENSION_PATHS_PROP = "io.xpipe.app.extensions";
private static AppProperties INSTANCE;
boolean fullVersion;
@ -48,13 +46,11 @@ public class AppProperties {
.orElse(UUID.randomUUID());
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
arch = System.getProperty("io.xpipe.app.arch");
staging = Optional.ofNullable(System.getProperty("io.xpipe.app.staging"))
.map(Boolean::parseBoolean)
.orElse(false);
staging = XPipeInstallation.isStaging();
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
.map(Boolean::parseBoolean)
.orElse(true);
dataDir = parseDataDir();
dataDir = XPipeInstallation.getDataDir();
showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase"))
.map(Boolean::parseBoolean)
.orElse(false);
@ -101,17 +97,6 @@ public class AppProperties {
return INSTANCE;
}
private Path parseDataDir() {
if (System.getProperty(DATA_DIR_PROP) != null) {
try {
return Path.of(System.getProperty(DATA_DIR_PROP));
} catch (InvalidPathException ignored) {
}
}
return Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe_stage" : ".xpipe");
}
public boolean isDeveloperMode() {
if (AppPrefs.get() == null) {
return false;

View file

@ -1,8 +1,8 @@
package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.modulefs.ModuleFileSystem;
import org.apache.commons.lang3.function.FailableConsumer;
import java.io.IOException;
import java.net.JarURLConnection;
@ -52,7 +52,7 @@ public class AppResources {
withResource(module, file, con);
}
public static void withResource(
public static void withResourceInLayer(
String module, String file, ModuleLayer layer, FailableConsumer<Path, IOException> con) {
try (var fs = FileSystems.newFileSystem(URI.create("module:/" + module), Map.of("layer", layer))) {
var f = fs.getPath(module.replace('.', '/') + "/resources/" + file);

View file

@ -1,6 +1,8 @@
package io.xpipe.app.core;
import lombok.Setter;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.util.UUID;
@ -12,6 +14,13 @@ public class AppState {
UUID userId;
boolean initialLaunch;
@NonFinal
@Setter
String userName;
@NonFinal
@Setter
String userEmail;
public AppState() {
UUID id = AppCache.get("userId", UUID.class, null);
if (id == null) {

View file

@ -4,6 +4,7 @@ import atlantafx.base.theme.*;
import com.jthemedetecor.OsThemeDetector;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
@ -13,6 +14,7 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
@ -23,8 +25,31 @@ import lombok.Getter;
public class AppTheme {
private static final PseudoClass LIGHT = PseudoClass.getPseudoClass("light");
private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark");
private static final PseudoClass PRETTY = PseudoClass.getPseudoClass("pretty");
private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
public static void initTheme(Window stage) {
var t = AppPrefs.get().theme.getValue();
if (t == null) {
return;
}
stage.getScene().getRoot().pseudoClassStateChanged(LIGHT, !t.getTheme().isDarkMode());
stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.getTheme().isDarkMode());
SimpleChangeListener.apply(AppPrefs.get().performanceMode(),val -> {
stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val);
stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val);
});
var transparent = AppPrefs.get().enableWindowTransparency().getValue();
stage.setOpacity(transparent ? t.getTransparencyOpacity() : 1.0);
}
public static void init() {
if (AppPrefs.get() == null) {
Application.setUserAgentStylesheet(Theme.getDefaultLightTheme().getTheme().getUserAgentStylesheet());
return;
}
@ -57,6 +82,15 @@ public class AppTheme {
AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(n);
});
AppPrefs.get().enableWindowTransparency().addListener((observable, oldValue, newValue) -> {
var th = AppPrefs.get().theme;
PlatformThread.runLaterIfNeeded(() -> {
for (Window window : Window.getWindows()) {
window.setOpacity(newValue ? th.get().getTransparencyOpacity() : 1.0);
}
});
});
}
private static void setDefault(boolean dark) {
@ -76,6 +110,7 @@ public class AppTheme {
for (Window window : Window.getWindows()) {
var scene = window.getScene();
Image snapshot = scene.snapshot(null);
initTheme(window);
Pane root = (Pane) scene.getRoot();
ImageView imageView = new ImageView(snapshot);
@ -102,13 +137,13 @@ public class AppTheme {
@AllArgsConstructor
@Getter
public enum Theme implements PrefsChoiceValue {
PRIMER_LIGHT("light", new PrimerLight()),
PRIMER_DARK("dark", new PrimerDark()),
NORD_LIGHT("nordLight", new NordLight()),
NORD_DARK("nordDark", new NordDark()),
CUPERTINO_LIGHT("cupertinoLight", new CupertinoLight()),
CUPERTINO_DARK("cupertinoDark", new CupertinoDark()),
DRACULA("dracula", new Dracula());
PRIMER_LIGHT("light", new PrimerLight(), 0.92),
PRIMER_DARK("dark", new PrimerDark(), 0.92),
NORD_LIGHT("nordLight", new NordLight(), 0.92),
NORD_DARK("nordDark", new NordDark(), 0.92),
CUPERTINO_LIGHT("cupertinoLight", new CupertinoLight(), 0.92),
CUPERTINO_DARK("cupertinoDark", new CupertinoDark(), 0.92),
DRACULA("dracula", new Dracula(), 0.92);
static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) {
@ -128,6 +163,7 @@ public class AppTheme {
private final String id;
private final atlantafx.base.theme.Theme theme;
private final double transparencyOpacity;
@Override
public String toTranslatedString() {

View file

@ -96,6 +96,11 @@ public class AppTray {
peerField.setAccessible(true);
var peer = peerField.get(this.privateTrayIcon);
// If tray initialization fails, this can be null
if (peer == null) {
return;
}
var canvasField = peer.getClass().getDeclaredField("canvas");
canvasField.setAccessible(true);
Component canvas = (Component) canvasField.get(peer);

View file

@ -41,17 +41,19 @@ public class AppWindowHelper {
public static void addIcons(Stage stage) {
stage.getIcons().clear();
for (String s : List.of(
"logo_16x16.png",
"logo_24x24.png",
"logo_32x32.png",
"logo_48x48.png",
"logo_128x128.png",
"logo_256x256.png")) {
if (AppImages.hasNormalImage("logo/" + s)) {
stage.getIcons().add(AppImages.image("logo/" + s));
// This allows for assigning logos even if AppImages has not been initialized yet
AppResources.with(AppResources.XPIPE_MODULE, "img/logo", path -> {
for (String s : List.of(
"logo_16x16.png",
"logo_24x24.png",
"logo_32x32.png",
"logo_48x48.png",
"logo_128x128.png",
"logo_256x256.png")) {
stage.getIcons().add(AppImages.loadImage(path.resolve(s)));
}
}
});
}
public static Stage sideWindow(

View file

@ -6,7 +6,7 @@ import io.xpipe.app.core.*;
import io.xpipe.app.issue.*;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.util.DefaultSecretValue;
import io.xpipe.app.util.FeatureProvider;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.core.impl.LocalStore;
@ -41,6 +41,7 @@ public class BaseMode extends OperationMode {
// Load translations before storage initialization to localize store error messages
// Also loaded before antivirus alert to localize that
AppI18n.init();
FeatureProvider.get().init();
AppAntivirusAlert.showIfNeeded();
LocalStore.init();
AppPrefs.init();
@ -61,20 +62,12 @@ public class BaseMode extends OperationMode {
public void finalTeardown() {
TrackEvent.info("mode", "Background mode shutdown started");
BrowserModel.DEFAULT.reset();
AppSocketServer.reset();
StoreViewState.reset();
DataStorage.reset();
AppPrefs.reset();
AppExtensionManager.reset();
// Shut down socket server last to keep a non-daemon thread running
AppSocketServer.reset();
TrackEvent.info("mode", "Background mode shutdown finished");
}
@Override
public ErrorHandler getErrorHandler() {
var log = new LogErrorHandler();
return new SyncErrorHandler(event -> {
log.handle(event);
ErrorAction.ignore().handle(event);
});
}
}

View file

@ -4,6 +4,7 @@ import io.xpipe.app.core.App;
import io.xpipe.app.core.AppGreetings;
import io.xpipe.app.core.AppMainWindow;
import io.xpipe.app.issue.*;
import io.xpipe.app.update.CommercializationAlert;
import io.xpipe.app.update.UpdateChangelogAlert;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.UnlockAlert;
@ -21,21 +22,23 @@ public class GuiMode extends PlatformMode {
@Override
public void onSwitchTo() throws Throwable {
super.platformSetup();
UnlockAlert.showIfNeeded();
UpdateChangelogAlert.showIfNeeded();
CommercializationAlert.showIfNeeded();
AppGreetings.showIfNeeded();
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
if (AppMainWindow.getInstance() == null) {
try {
TrackEvent.info("mode", "Setting up window ...");
UnlockAlert.showIfNeeded();
App.getApp().setupWindow();
AppGreetings.showIfNeeded();
UpdateChangelogAlert.showIfNeeded();
AppMainWindow.getInstance().show();
latch.countDown();
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).terminal(true).handle();
}
} else {
AppMainWindow.getInstance().show();
latch.countDown();
}
});
@ -59,10 +62,6 @@ public class GuiMode extends PlatformMode {
@Override
public ErrorHandler getErrorHandler() {
var log = new LogErrorHandler();
return new SyncErrorHandler(event -> {
log.handle(event);
ErrorHandlerComp.showAndTryWait(event, false);
});
return new SyncErrorHandler(new GuiErrorHandler());
}
}

View file

@ -2,15 +2,15 @@ package io.xpipe.app.core.mode;
import io.xpipe.app.core.*;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ErrorHandler;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.issue.*;
import io.xpipe.app.launcher.LauncherCommand;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeSession;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeInstallation;
import io.xpipe.core.util.XPipeSystemId;
import org.apache.commons.lang3.function.FailableRunnable;
import java.util.ArrayList;
import java.util.List;
@ -170,25 +170,41 @@ public abstract class OperationMode {
return ALL;
}
public static void restart() {
OperationMode.executeAfterShutdown(() -> {
var exec = XPipeInstallation.createExternalAsyncLaunchCommand(XPipeInstallation.getLocalDefaultInstallationBasePath(), XPipeDaemonMode.GUI, "");
LocalStore.getShell().executeSimpleCommand(exec);
});
}
public static void executeAfterShutdown(FailableRunnable<Exception> r) {
if (inShutdown) {
return;
}
inShutdown = true;
inShutdownHook = false;
try {
if (CURRENT != null) {
CURRENT.finalTeardown();
// Creates separate non daemon thread to force execution after shutdown even if current thread is a daemon
var t = new Thread(() -> {
if (inShutdown) {
return;
}
CURRENT = null;
r.run();
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).build().handle();
OperationMode.halt(1);
}
OperationMode.halt(0);
inShutdown = true;
inShutdownHook = false;
try {
if (CURRENT != null) {
CURRENT.finalTeardown();
}
CURRENT = null;
r.run();
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).build().handle();
OperationMode.halt(1);
}
OperationMode.halt(0);
});
t.setDaemon(false);
t.start();
try {
t.join();
} catch (InterruptedException ignored) {
}
}
public static void halt(int code) {
@ -275,5 +291,7 @@ public abstract class OperationMode {
public abstract void finalTeardown() throws Throwable;
public abstract ErrorHandler getErrorHandler();
public ErrorHandler getErrorHandler() {
return new SyncErrorHandler(new GuiErrorHandler());
}
}

View file

@ -5,7 +5,7 @@ import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.cli.DialogExchange;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.dialog.DialogReference;
import org.apache.commons.lang3.function.FailableConsumer;
import io.xpipe.core.util.FailableConsumer;
import java.util.HashMap;
import java.util.Map;

View file

@ -3,9 +3,10 @@ package io.xpipe.app.exchange;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.LaunchExchange;
import io.xpipe.core.store.LaunchableStore;
import org.apache.commons.exec.CommandLine;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LaunchExchangeImpl extends LaunchExchange
implements MessageExchangeImpl<LaunchExchange.Request, LaunchExchange.Response> {
@ -15,10 +16,19 @@ public class LaunchExchangeImpl extends LaunchExchange
var store = getStoreEntryById(msg.getId(), false);
if (store.getStore() instanceof LaunchableStore s) {
var command = s.prepareLaunchCommand(store.getName());
var split = CommandLine.parse(command);
return Response.builder().command(List.of(split.toStrings())).build();
return Response.builder().command(split(command)).build();
}
throw new IllegalArgumentException(store.getName() + " is not launchable");
}
private List<String> split(String command) {
var split = Arrays.stream(command.split(" ", 3)).collect(Collectors.toList());
var s = split.get(2);
if ((s.startsWith("\"") && s.endsWith("\""))
|| (s.startsWith("'") && s.endsWith("'"))) {
split.set(2,s.substring(1, s.length() - 1));
}
return split;
}
}

View file

@ -6,7 +6,9 @@ import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ServiceLoader;
@ -49,6 +51,10 @@ public interface ActionProvider {
void execute() throws Exception;
}
default String getId() {
return null;
}
default boolean isActive() {
return true;
}
@ -57,6 +63,19 @@ public interface ActionProvider {
String getId();
Action createAction(URI uri) throws Exception;
}
interface XPipeLauncherCallSite extends LauncherCallSite {
String getId();
default Action createAction(URI uri) throws Exception {
var args = new ArrayList<>(Arrays.asList(uri.getPath().substring(1).split("/")));
args.add(0, uri.getHost());
return createAction(args);
}
Action createAction(List<String> args) throws Exception;
}
@ -95,6 +114,14 @@ public interface ActionProvider {
ALWAYS_ENABLE
}
default boolean isSystemAction() {
return false;
}
default boolean canLinkTo() {
return false;
}
Action createAction(T store);
Class<T> getApplicableClass();

View file

@ -2,13 +2,11 @@ package io.xpipe.app.ext;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.base.SystemStateComp;
import io.xpipe.app.comp.storage.store.StandardStoreEntryComp;
import io.xpipe.app.comp.storage.store.StoreSectionComp;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.comp.storage.store.StoreSection;
import io.xpipe.app.comp.storage.store.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.*;
@ -21,6 +19,10 @@ import java.util.List;
public interface DataStoreProvider {
default boolean alwaysShowSummary() {
return false;
}
default ModuleInstall getRequiredAdditionalInstallation() {
return null;
}
@ -34,22 +36,23 @@ public interface DataStoreProvider {
}
}
default boolean shouldShowInSelectionTree() {
return true;
}
default void preAdd(DataStore store) {}
default String browserDisplayName(DataStore store) {
var e = DataStorage.get().getStoreDisplayName(store);
return e.orElse("?");
}
default boolean shouldEdit() {
return false;
}
default Comp<?> customDisplay(StoreSection s) {
return new StandardStoreEntryComp(s.getWrapper(), null);
default Comp<?> customEntryComp(StoreSection s, boolean preferLarge) {
return StoreEntryComp.create(s.getWrapper(),true, null, preferLarge);
}
default Comp<?> customContainer(StoreSection section) {
return new StoreSectionComp(section);
default Comp<?> customSectionComp(StoreSection section, boolean topLevel) {
return new StoreSectionComp(section, topLevel);
}
default boolean canHaveSubShells() {

View file

@ -3,9 +3,9 @@ package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.ModuleLayerLoader;
import lombok.Value;
import org.apache.commons.lang3.function.FailableRunnable;
import java.util.Comparator;
import java.util.List;

View file

@ -31,10 +31,18 @@ public abstract class Comp<S extends CompStructure<?>> {
return of(() -> new Region());
}
public static Comp<CompStructure<Spacer>> spacer(double size) {
public static Comp<CompStructure<Spacer>> hspacer(double size) {
return of(() -> new Spacer(size));
}
public static Comp<CompStructure<Spacer>> hspacer() {
return of(() -> new Spacer(Orientation.HORIZONTAL));
}
public static Comp<CompStructure<Spacer>> vspacer(double size) {
return of(() -> new Spacer(size, Orientation.VERTICAL));
}
public static <R extends Region> Comp<CompStructure<R>> of(Supplier<R> r) {
return new Comp<>() {
@Override

View file

@ -37,7 +37,6 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
if (show.test(event)) {
var cm = contextMenu.get();
if (cm != null) {
cm.setAutoHide(true);
cm.show(r, event.getScreenX(), event.getScreenY());
currentContextMenu = cm;
}

View file

@ -4,12 +4,12 @@ import io.xpipe.app.fxcomps.CompStructure;
import javafx.css.PseudoClass;
import javafx.scene.input.DragEvent;
public class DragPseudoClassAugment<S extends CompStructure<?>> implements Augment<S> {
public class DragOverPseudoClassAugment<S extends CompStructure<?>> implements Augment<S> {
public static final PseudoClass DRAGGED_PSEUDOCLASS = PseudoClass.getPseudoClass("drag-over");
public static <S extends CompStructure<?>> DragPseudoClassAugment<S> create() {
return new DragPseudoClassAugment<>();
public static <S extends CompStructure<?>> DragOverPseudoClassAugment<S> create() {
return new DragOverPseudoClassAugment<>();
}
@Override

View file

@ -0,0 +1,61 @@
package io.xpipe.app.fxcomps.augment;
import io.xpipe.app.fxcomps.CompStructure;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.input.MouseEvent;
public class DraggableAugment<S extends CompStructure<?>> implements Augment<S> {
double lastMouseX = 0, lastMouseY = 0;
public static <S extends CompStructure<?>> DraggableAugment<S> create() {
return new DraggableAugment<>();
}
@Override
public void augment(S struc) {
var circle = struc.get();
var oldDepth = struc.get().getViewOrder();
circle.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
lastMouseX = mouseEvent.getSceneX();
lastMouseY = mouseEvent.getSceneY();
circle.getScene().setCursor(Cursor.MOVE);
circle.setViewOrder(1000);
}
});
circle.setOnMouseReleased(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
circle.getScene().setCursor(Cursor.HAND);
}
});
circle.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
final double deltaX = mouseEvent.getSceneX() - lastMouseX;
final double deltaY = mouseEvent.getSceneY() - lastMouseY;
final double initialTranslateX = circle.getTranslateX();
final double initialTranslateY = circle.getTranslateY();
circle.setTranslateX(initialTranslateX + deltaX);
circle.setTranslateY(initialTranslateY + deltaY);
lastMouseX = mouseEvent.getSceneX();
lastMouseY = mouseEvent.getSceneY();
}
});
circle.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
circle.getScene().setCursor(Cursor.HAND);
}
}
});
circle.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
circle.getScene().setCursor(Cursor.DEFAULT);
}
}
});
}
}

View file

@ -27,9 +27,17 @@ public class GrowAugment<S extends CompStructure<?>> implements Augment<S> {
if (width) {
r.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> p.getWidth()
- p.getInsets().getLeft()
- p.getInsets().getRight(),
() -> {
var val = p.getWidth()
- p.getInsets().getLeft()
- p.getInsets().getRight();
if (val <= 0) {
return Region.USE_COMPUTED_SIZE;
}
// Floor to prevent rounding issues which cause an infinite growing
return Math.floor(val);
},
p.widthProperty(),
p.insetsProperty()));
}
@ -43,7 +51,9 @@ public class GrowAugment<S extends CompStructure<?>> implements Augment<S> {
if (val <= 0) {
return Region.USE_COMPUTED_SIZE;
}
return val;
// Floor to prevent rounding issues which cause an infinite growing
return Math.floor(val);
},
p.heightProperty(),
p.insetsProperty()));

View file

@ -1,29 +1,43 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.comp.storage.store.StoreEntryFlatMiniSectionComp;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.storage.store.*;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.RequiredArgsConstructor;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Locale;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
@AllArgsConstructor
@RequiredArgsConstructor
public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
public static <T extends DataStore> DataStoreChoiceComp<T> other(Property<T> selected, Class<T> clazz, Predicate<T> filter) {
public static <T extends DataStore> DataStoreChoiceComp<T> other(
Property<T> selected, Class<T> clazz, Predicate<T> filter) {
return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter);
}
@ -60,9 +74,97 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
private final Class<T> storeClass;
private final Predicate<T> applicableCheck;
private Popover popover;
private Popover getPopover() {
if (popover == null) {
var selectedCategory = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue());
var filterText = new SimpleStringProperty();
popover = new Popover();
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
var e = storeEntryWrapper.getEntry();
if (e.getStore() == self) {
return false;
}
var store = e.getStore();
if (!(mode == Mode.ENVIRONMENT)
&& e.getProvider() != null
&& !e.getProvider().canHaveSubShells()) {
return false;
}
// Check if load failed
if (e.getStore() == null) {
return false;
}
return storeClass.isAssignableFrom(e.getStore().getClass()) && e.getState().isUsable() && applicableCheck.test(e.getStore().asNeeded());
};
var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory),
(s, comp) -> {
comp.apply(struc -> struc.get().setOnAction(event -> {
selected.setValue(
s.getWrapper().getEntry().getStore().asNeeded());
popover.hide();
event.consume();
}));
if (!applicable.test(s.getWrapper())) {
comp.disable(new SimpleBooleanProperty(true));
}
});
var category = new DataStoreCategoryChoiceComp(selectedCategory).styleClass(Styles.LEFT_PILL);
var filter = new FilterComp(filterText)
.styleClass(Styles.CENTER_PILL)
.hgrow()
.apply(struc -> {
popover.setOnShowing(event -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
struc.getText().requestFocus();
});
});
});
});
});
var addButton = Comp.of(() -> {
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
StoreCreationMenu.addButtons(m);
return m;
}).padding(new Insets(-2)).styleClass(Styles.RIGHT_PILL).grow(false, true);
var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton))
.styleClass("top")
.apply(struc -> struc.get().setFillHeight(true))
.createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);
content.getStyleClass().add("choice-comp-content");
content.setPrefWidth(500);
content.setMaxHeight(550);
popover.setContentNode(content);
popover.setCloseButtonEnabled(true);
popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);
popover.setHeaderAlwaysVisible(true);
popover.setDetachable(true);
popover.setTitle(AppI18n.get("selectConnection"));
AppFont.small(popover.getContentNode());
}
return popover;
}
protected Region createGraphic(T s) {
var provider = DataStoreProviders.byStore(s);
var imgView = new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName(s)), 16, 16)
var imgView = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s), 16)
.createRegion();
var name = DataStorage.get().getUsableStores().stream()
@ -81,6 +183,10 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
}
private String toName(DataStore store) {
if (store == null) {
return null;
}
if (mode == Mode.PROXY && store instanceof ShellStore && ShellStore.isLocal(store.asNeeded())) {
return AppI18n.get("none");
}
@ -89,54 +195,53 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
}
@Override
@SuppressWarnings("unchecked")
protected Region createSimple() {
var list = StoreEntryFlatMiniSectionComp.ALL;
var comboBox = new CustomComboBoxBuilder<T>(
selected,
t -> list.stream()
.filter(e -> t.equals(e.getEntry().getStore()))
.findFirst()
.orElseThrow()
.createRegion(),
new Label(AppI18n.get("none")),
n -> true);
var button = new ButtonComp(
Bindings.createStringBinding(
() -> {
return toName(selected.getValue());
},
selected),
() -> {});
button.apply(struc -> {
struc.get().setMaxWidth(2000);
struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get()
.setGraphic(PrettyImageHelper.ofSvg(Bindings.createStringBinding(
() -> {
if (selected.getValue() == null) {
return null;
}
if (list.size() > 5) {
comboBox.addFilter((t, s) -> {
var entry = DataStorage.get().getStoreDisplayName(t).orElse("?");
return entry.toLowerCase(Locale.ROOT).contains(s.toLowerCase(Locale.ROOT));
});
}
return DataStorage.get()
.getStoreEntryIfPresent(selected.getValue())
.map(entry -> entry.getProvider()
.getDisplayIconFileName(selected.getValue()))
.orElse(null);
},
selected),
16,
16)
.createRegion());
struc.get().setOnAction(event -> {
getPopover().show(struc.get());
event.consume();
});
})
.styleClass("choice-comp");
comboBox.setAccessibleNames(t -> toName(t));
comboBox.setSelectedDisplay(t -> createGraphic(t));
comboBox.setUnknownNode(t -> createGraphic(t));
for (var e : list) {
if (e.getEntry().getStore() == self) {
continue;
}
var s = e.getEntry().getStore();
if (!(mode == Mode.ENVIRONMENT) && e.getEntry().getProvider() != null && !e.getEntry().getProvider().canHaveSubShells()) {
continue;
}
// Check if load failed
if (e.getEntry().getStore() == null) {
continue;
}
var node = comboBox.add((T) e.getEntry().getStore());
if (!storeClass.isAssignableFrom(s.getClass()) || !applicableCheck.test((T) s)) {
comboBox.disable(node);
}
}
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("choice-comp");
cb.setMaxWidth(2000);
return cb;
var r = button.grow(true, false).createRegion();
var icon = new FontIcon("mdal-keyboard_arrow_down");
icon.setDisable(true);
icon.setPickOnBounds(false);
AppFont.header(icon);
var pane = new StackPane(r, icon);
StackPane.setMargin(icon, new Insets(10));
pane.setPickOnBounds(false);
StackPane.setAlignment(icon, Pos.CENTER_RIGHT);
pane.setMaxWidth(2000);
r.prefWidthProperty().bind(pane.widthProperty());
r.maxWidthProperty().bind(pane.widthProperty());
return pane;
}
}

View file

@ -6,7 +6,6 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
@ -30,13 +29,13 @@ public class FileSystemStoreChoiceComp extends SimpleComp {
private Region createGraphic(FileSystemStore s) {
var provider = DataStoreProviders.byStore(s);
var img = new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName(s)), 16, 16);
var img = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s), 16);
return new Label(getName(s), img.createRegion());
}
private Region createDisplayGraphic(FileSystemStore s) {
var provider = DataStoreProviders.byStore(s);
var img = new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName(s)), 16, 16);
var img = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s), 16);
return new Label(null, img.createRegion());
}

View file

@ -29,7 +29,7 @@ public class FilterComp extends Comp<FilterComp.Structure> {
public Structure createBase() {
var fi = new FontIcon("mdi2m-magnify");
var bgLabel = new Label("Search ...", fi);
bgLabel.getStyleClass().add("background");
bgLabel.getStyleClass().add("filter-background");
var filter = new TextField();
filter.setAccessibleText("Filter");

View file

@ -21,6 +21,11 @@ public class IconButtonComp extends Comp<CompStructure<Button>> {
this(new SimpleObjectProperty<>(defaultVal), null);
}
public IconButtonComp(ObservableValue<String> icon) {
this.icon = icon;
this.listener = null;
}
public IconButtonComp(String defaultVal, Runnable listener) {
this(new SimpleObjectProperty<>(defaultVal), listener);
}
@ -47,9 +52,9 @@ public class IconButtonComp extends Comp<CompStructure<Button>> {
// fi.iconColorProperty().bind(button.textFillProperty());
button.setGraphic(fi);
button.setOnAction(e -> {
e.consume();
if (listener != null) {
listener.run();
e.consume();
}
});
button.getStyleClass().add("icon-button-comp");

View file

@ -7,7 +7,6 @@ import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
@ -23,7 +22,7 @@ public class PrettyImageComp extends SimpleComp {
private final double width;
private final double height;
public PrettyImageComp(ObservableValue<String> value, double width, double height) {
PrettyImageComp(ObservableValue<String> value, double width, double height) {
this.value = value;
this.width = width;
this.height = height;
@ -31,9 +30,17 @@ public class PrettyImageComp extends SimpleComp {
@Override
protected Region createSimple() {
var aspectRatioProperty = new SimpleDoubleProperty(1);
var imageAspectRatioProperty = new SimpleDoubleProperty(1);
var svgAspectRatioProperty = new SimpleDoubleProperty(1);
var storeIcon = new ImageView();
var aspectRatioProperty = Bindings.createDoubleBinding(
() -> {
if (storeIcon.getImage() == null) {
return 1.0;
}
return storeIcon.getImage().getWidth()
/ storeIcon.getImage().getHeight();
},
storeIcon.imageProperty());
var widthProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < aspectRatioProperty.doubleValue();
@ -57,94 +64,38 @@ public class PrettyImageComp extends SimpleComp {
var image = new SimpleStringProperty();
var stack = new StackPane();
{
var svgImageContent = new SimpleStringProperty();
var storeIcon = SvgView.create(svgImageContent);
SimpleChangeListener.apply(image, newValue -> {
if (newValue == null) {
svgImageContent.set(null);
return;
}
storeIcon.setFocusTraversable(false);
storeIcon
.imageProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (image.get() == null) {
return null;
}
if (AppImages.hasSvgImage(newValue)) {
svgImageContent.set(AppImages.svgImage(newValue));
} else if (AppImages.hasSvgImage(newValue.replace("-dark", ""))) {
svgImageContent.set(AppImages.svgImage(newValue.replace("-dark", "")));
} else {
svgImageContent.set(null);
}
});
var ar = Bindings.createDoubleBinding(
() -> {
return storeIcon.getWidth().getValue().doubleValue()
/ storeIcon.getHeight().getValue().doubleValue();
},
storeIcon.getWidth(),
storeIcon.getHeight());
svgAspectRatioProperty.bind(ar);
var node = storeIcon.createWebview();
node.prefWidthProperty().bind(widthProperty);
node.maxWidthProperty().bind(widthProperty);
node.minWidthProperty().bind(widthProperty);
node.prefHeightProperty().bind(heightProperty);
node.maxHeightProperty().bind(heightProperty);
node.minHeightProperty().bind(heightProperty);
stack.getChildren().add(node);
}
if (AppImages.hasNormalImage(image.getValue())) {
return AppImages.image(image.getValue());
} else if (AppImages.hasNormalImage(image.getValue().replace("-dark", ""))) {
return AppImages.image(image.getValue().replace("-dark", ""));
} else {
return null;
}
},
image));
storeIcon.fitWidthProperty().bind(widthProperty);
storeIcon.fitHeightProperty().bind(heightProperty);
storeIcon.setSmooth(true);
stack.getChildren().add(storeIcon);
{
var storeIcon = new ImageView();
storeIcon.setFocusTraversable(false);
storeIcon
.imageProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (image.get() == null) {
return null;
}
if (AppImages.hasNormalImage(image.getValue())) {
return AppImages.image(image.getValue());
} else if (AppImages.hasNormalImage(image.getValue().replace("-dark", ""))) {
return AppImages.image(image.getValue().replace("-dark", ""));
} else {
return null;
}
},
image));
var ar = Bindings.createDoubleBinding(
() -> {
if (storeIcon.getImage() == null) {
return 1.0;
}
return storeIcon.getImage().getWidth()
/ storeIcon.getImage().getHeight();
},
storeIcon.imageProperty());
imageAspectRatioProperty.bind(ar);
storeIcon.fitWidthProperty().bind(widthProperty);
storeIcon.fitHeightProperty().bind(heightProperty);
storeIcon.setSmooth(true);
stack.getChildren().add(storeIcon);
}
Consumer<String> update = val -> {
var fixed = val != null ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().getTheme().isDarkMode() ? "-dark" : "") + "." + FileNames.getExtension(val) : null;
image.set(fixed);
aspectRatioProperty.unbind();
if (val == null) {
stack.getChildren().get(0).setOpacity(0.0);
stack.getChildren().get(1).setOpacity(0.0);
} else if (val.endsWith(".svg")) {
aspectRatioProperty.bind(svgAspectRatioProperty);
stack.getChildren().get(0).setOpacity(1.0);
stack.getChildren().get(1).setOpacity(0.0);
stack.getChildren().get(0).setVisible(false);
} else {
aspectRatioProperty.bind(imageAspectRatioProperty);
stack.getChildren().get(0).setOpacity(0.0);
stack.getChildren().get(1).setOpacity(1.0);
stack.getChildren().get(0).setVisible(true);
}
};
@ -159,6 +110,7 @@ public class PrettyImageComp extends SimpleComp {
stack.setPrefHeight(height);
stack.setMinHeight(height);
stack.setAlignment(Pos.CENTER);
stack.getStyleClass().add("stack");
return stack;
}
}

View file

@ -0,0 +1,42 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.core.impl.FileNames;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
public class PrettyImageHelper {
public static Comp<?> ofFixedSquare(String img, int size) {
if (img.endsWith(".svg")) {
var base = FileNames.getBaseName(img);
var renderedName = base + "-" + size + ".png";
if (AppImages.hasNormalImage(base + "-" + size + ".png")) {
return new PrettyImageComp(new SimpleStringProperty(renderedName), size, size);
} else {
return new PrettySvgComp(new SimpleStringProperty(img), size, size);
}
}
return new PrettyImageComp(new SimpleStringProperty(img), size, size);
}
public static Comp<?> ofFixed(String img, int w, int h) {
if (w == h) {
return ofFixedSquare(img, w);
}
return img.endsWith(".svg") ? new PrettySvgComp(new SimpleStringProperty(img), w, h) : new PrettyImageComp(new SimpleStringProperty(img), w, h);
}
public static Comp<?> ofSvg(ObservableValue<String> img, int w, int h) {
return new PrettySvgComp(img, w, h);
}
public static Comp<?> ofFixedSmallSquare(String img) {
return ofFixed(img, 16, 16);
}
}

View file

@ -0,0 +1,106 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.function.Consumer;
public class PrettySvgComp extends SimpleComp {
private final ObservableValue<String> value;
private final double width;
private final double height;
public PrettySvgComp(ObservableValue<String> value, double width, double height) {
this.value = value;
this.width = width;
this.height = height;
}
@Override
protected Region createSimple() {
var image = new SimpleStringProperty();
var syncValue = PlatformThread.sync(value);
var storeIcon = SvgView.create(Bindings.createObjectBinding(
() -> {
if (image.get() == null) {
return null;
}
if (AppImages.hasSvgImage(image.getValue())) {
return AppImages.svgImage(image.getValue());
} else if (AppImages.hasSvgImage(image.getValue().replace("-dark", ""))) {
return AppImages.svgImage(image.getValue().replace("-dark", ""));
} else {
return null;
}
},
image));
var ar = Bindings.createDoubleBinding(
() -> {
return storeIcon.getWidth().getValue().doubleValue()
/ storeIcon.getHeight().getValue().doubleValue();
},
storeIcon.getWidth(),
storeIcon.getHeight());
var widthProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < ar.doubleValue();
if (widthLimited) {
return width;
} else {
return height * ar.doubleValue();
}
},
ar);
var heightProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < ar.doubleValue();
if (widthLimited) {
return width / ar.doubleValue();
} else {
return height;
}
},
ar);
var stack = new StackPane();
var node = storeIcon.createWebview();
node.prefWidthProperty().bind(widthProperty);
node.maxWidthProperty().bind(widthProperty);
node.minWidthProperty().bind(widthProperty);
node.prefHeightProperty().bind(heightProperty);
node.maxHeightProperty().bind(heightProperty);
node.minHeightProperty().bind(heightProperty);
stack.getChildren().add(node);
Consumer<String> update = val -> {
var fixed = val != null ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().getTheme().isDarkMode() ? "-dark" : "") + "." + FileNames.getExtension(val) : null;
image.set(fixed);
};
SimpleChangeListener.apply(syncValue, val -> update.accept(val));
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
update.accept(syncValue.getValue());
});
stack.setFocusTraversable(false);
stack.setPrefWidth(width);
stack.setMinWidth(width);
stack.setPrefHeight(height);
stack.setMinHeight(height);
stack.setAlignment(Pos.CENTER);
stack.getStyleClass().add("stack");
return stack;
}
}

Some files were not shown because too many files have changed in this diff Show more