Initial functionality
This commit is contained in:
commit
9e5494035a
31 changed files with 3668 additions and 0 deletions
33
.github/workflows/pr_check.yml
vendored
Normal file
33
.github/workflows/pr_check.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: pr_check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
profile: minimal
|
||||
- name: Install trunk
|
||||
uses: jetli/trunk-action@v0.1.0
|
||||
with:
|
||||
version: 'latest'
|
||||
- name: Build
|
||||
run: trunk build
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/dist/
|
||||
/target/
|
2316
Cargo.lock
generated
Normal file
2316
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
57
Cargo.toml
Normal file
57
Cargo.toml
Normal file
|
@ -0,0 +1,57 @@
|
|||
[package]
|
||||
name = "yarrr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Template for starting a Yew project using Trunk"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/yewstack/yew-trunk-minimal-template"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["yew", "trunk"]
|
||||
categories = ["gui", "wasm", "web-programming"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[[bin]]
|
||||
name = "frontend"
|
||||
|
||||
[[bin]]
|
||||
name = "backend"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11.8", features = ["json"] }
|
||||
serde = { version = "1.0.132", features = ["derive"] }
|
||||
uuid = { version = "1.0.0", features = ["serde"] }
|
||||
futures = "0.3"
|
||||
bytes = "1.0"
|
||||
yew_icons = {version = "0.7", features = [
|
||||
"FeatherFolder",
|
||||
"FeatherCornerLeftUp",
|
||||
"FeatherFilm",
|
||||
"FeatherImage",
|
||||
"FeatherFile",
|
||||
"FeatherRefreshCcw"
|
||||
] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
yew = { version = "0.20", features = [ "csr", "hydration" ] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Window",
|
||||
] }
|
||||
wasm-logger = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
yew = { version = "0.20", features = [ "ssr" ] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
actix-web = { version = "4.3" }
|
||||
actix-files = { version = "0.6" }
|
||||
actix-web-httpauth = { version = "0.8" }
|
||||
clap = { version = "3.1.7", features = ["derive"] }
|
||||
url-escape = { version = "0.1.1" }
|
||||
rand = "0.8"
|
177
LICENSE-APACHE
Normal file
177
LICENSE-APACHE
Normal file
|
@ -0,0 +1,177 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
25
LICENSE-MIT
Normal file
25
LICENSE-MIT
Normal file
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) NGnius (Graham) <ngniusness@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
74
README.md
Normal file
74
README.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
# Yew Trunk Template
|
||||
|
||||
This is a fairly minimal template for a Yew app that's built with [Trunk].
|
||||
|
||||
## Usage
|
||||
|
||||
For a more thorough explanation of Trunk and its features, please head over to the [repository][trunk].
|
||||
|
||||
### Installation
|
||||
|
||||
If you don't already have it installed, it's time to install Rust: <https://www.rust-lang.org/tools/install>.
|
||||
The rest of this guide assumes a typical Rust installation which contains both `rustup` and Cargo.
|
||||
|
||||
To compile Rust to WASM, we need to have the `wasm32-unknown-unknown` target installed.
|
||||
If you don't already have it, install it with the following command:
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
Now that we have our basics covered, it's time to install the star of the show: [Trunk].
|
||||
Simply run the following command to install it:
|
||||
|
||||
```bash
|
||||
cargo install trunk wasm-bindgen-cli
|
||||
```
|
||||
|
||||
That's it, we're done!
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
trunk serve
|
||||
```
|
||||
|
||||
Rebuilds the app whenever a change is detected and runs a local server to host it.
|
||||
|
||||
There's also the `trunk watch` command which does the same thing but without hosting it.
|
||||
|
||||
### Release
|
||||
|
||||
```bash
|
||||
trunk build --release
|
||||
```
|
||||
|
||||
This builds the app in release mode similar to `cargo build --release`.
|
||||
You can also pass the `--release` flag to `trunk serve` if you need to get every last drop of performance.
|
||||
|
||||
Unless overwritten, the output will be located in the `dist` directory.
|
||||
|
||||
## Using this template
|
||||
|
||||
There are a few things you have to adjust when adopting this template.
|
||||
|
||||
### Remove example code
|
||||
|
||||
The code in [src/main.rs](src/main.rs) specific to the example is limited to only the `view` method.
|
||||
There is, however, a fair bit of Sass in [index.scss](index.scss) you can remove.
|
||||
|
||||
### Update metadata
|
||||
|
||||
Update the `name`, `version`, `description` and `repository` fields in the [Cargo.toml](Cargo.toml) file.
|
||||
The [index.html](index.html) file also contains a `<title>` tag that needs updating.
|
||||
|
||||
Finally, you should update this very `README` file to be about your app.
|
||||
|
||||
### License
|
||||
|
||||
The template ships with both the Apache and MIT license.
|
||||
If you don't want to have your app dual licensed, just remove one (or both) of the files and update the `license` field in `Cargo.toml`.
|
||||
|
||||
There are two empty spaces in the MIT license you need to fill out: `` and `NGnius (Graham) <ngniusness@gmail.com>`.
|
||||
|
||||
[trunk]: https://github.com/thedodd/trunk
|
10
index.html
Normal file
10
index.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Yarrr</title>
|
||||
<link data-trunk rel="rust" data-bin="frontend" />
|
||||
<link data-trunk rel="sass" href="index.scss" />
|
||||
</head>
|
||||
</html>
|
186
index.scss
Normal file
186
index.scss
Normal file
|
@ -0,0 +1,186 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
//display: flex;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
|
||||
background: #444444;
|
||||
font-size: 1.5rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
main {
|
||||
color: #cccccc;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
padding: 2em 4em;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 20em;
|
||||
}
|
||||
|
||||
div.yarrr-rant {
|
||||
padding: 5%;
|
||||
margin: 2%;
|
||||
background: #cccccc;
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
.yarrr-rant-quote {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.yarrr-rant-quote:before {
|
||||
content: "\"";
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.yarrr-rant-quote:after {
|
||||
content: "\"";
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.yarrr-rant-quote-attrib {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.yarrr-rant-quote-attrib:before {
|
||||
content: "-";
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.yarrr-rant-p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.yarrr-proof-of-purchase {
|
||||
height: 6em;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.yarrr-files-browser {
|
||||
text-align: left;
|
||||
width: 92%;
|
||||
margin: auto;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.yarrr-files-browser-header {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
color: #cccccc;
|
||||
margin: auto;
|
||||
padding: 1%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.yarrr-files-browser-header-refresh {
|
||||
background: #cccccc;
|
||||
color: #444444;
|
||||
border: 4px solid #666666;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
//display: inline-block;
|
||||
margin: auto;
|
||||
margin-right: 2%;
|
||||
padding: auto;
|
||||
//float: right;
|
||||
//width: 1em;
|
||||
}
|
||||
|
||||
.yarrr-files-browser-header-refresh:hover {
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
.yarrr-files-browser-header-refresh:active {
|
||||
color: #111111;
|
||||
border: 4px solid #111111;
|
||||
}
|
||||
|
||||
.yarrr-files-browser-entries {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.yarrr-file-entry {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
background: #cccccc;
|
||||
color: #444444;
|
||||
border: 4px solid #666666;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.yarrr-file-entry:active {
|
||||
border: 4px solid #111111;
|
||||
}
|
||||
|
||||
.yarrr-file-entry-dir {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.yarrr-file-entry-file {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a.yarrr-file-entry-link {}
|
||||
|
||||
a.yarrr-file-entry-link:visited {
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
a.yarrr-file-entry-link:link {
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
a.yarrr-file-entry-link:active {
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
a.yarrr-file-entry-link:hover {
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
.yarrr-file-entry-icon {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.yarrr-file-entry-name {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
}
|
BIN
misc/banner.png
Normal file
BIN
misc/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
4
run.sh
Executable file
4
run.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
#trunk build ./index.html && cargo run --bin backend -- localhost:7534
|
||||
trunk build ./index.html && cargo run --bin backend -- localhost:7534 --dir /mnt/nas/media -u user -p password
|
67
src/api/get_args.rs
Normal file
67
src/api/get_args.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use std::sync::RwLock;
|
||||
use clap::Parser;
|
||||
|
||||
static ARGS: RwLock<Option<CliArgs>> = RwLock::new(None);
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct CliArgs {
|
||||
/// Path to storage root
|
||||
#[clap(long)]
|
||||
pub dir: Option<std::path::PathBuf>,
|
||||
/// Domain name
|
||||
pub domain: String,
|
||||
/// Force HTTPS?
|
||||
#[clap(long)]
|
||||
pub ssl: bool,
|
||||
/// Username for basic auth
|
||||
#[clap(short, long)]
|
||||
pub username: Option<String>,
|
||||
/// Password for basic auth
|
||||
#[clap(short, long)]
|
||||
pub password: Option<String>,
|
||||
/// Display hidden files
|
||||
#[clap(short, long)]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
fn cli() -> Self {
|
||||
Self::parse()
|
||||
.validate()
|
||||
}
|
||||
|
||||
fn validate(self) -> Self {
|
||||
if self.dir.is_some() {
|
||||
assert!(self.username.is_some(), "Credentials must be used to secure the hosted dir");
|
||||
assert!(self.password.is_some(), "Credentials must be used to secure the hosted dir");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get() -> Self {
|
||||
if let Some(args) = ARGS.read().ok().map(|x| x.clone()).flatten() {
|
||||
args
|
||||
} else {
|
||||
let mut lock = ARGS.write().expect("Failed to acquire ARGS write lock");
|
||||
let args = Self::cli();
|
||||
*lock = Some(args.clone());
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate(&self, username: &str, password: &str) -> bool {
|
||||
let mut allowed = true;
|
||||
if let Some(expected_user) = &self.username {
|
||||
allowed &= expected_user == username;
|
||||
}
|
||||
if let Some(expected_password) = &self.password {
|
||||
allowed &= expected_password == password;
|
||||
}
|
||||
if !allowed {
|
||||
let duration_ms = rand::random::<u16>() & 0x7f_ff; // max timeout 2^15 (~32s)
|
||||
tokio::time::sleep(std::time::Duration::from_millis(duration_ms as _)).await;
|
||||
}
|
||||
allowed
|
||||
}
|
||||
}
|
14
src/api/get_banner.rs
Normal file
14
src/api/get_banner.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use actix_web::{get, Responder};
|
||||
|
||||
#[get("/banner.png")]
|
||||
pub async fn bruce() -> impl Responder {
|
||||
let banner1 = std::path::PathBuf::from("./banner.png");
|
||||
if banner1.exists() {
|
||||
return actix_files::NamedFile::open_async(banner1).await;
|
||||
}
|
||||
let banner2 = std::path::PathBuf::from("./misc/banner.png");
|
||||
if banner2.exists() {
|
||||
return actix_files::NamedFile::open_async(banner2).await;
|
||||
}
|
||||
actix_files::NamedFile::open_async("./banner.jpg").await
|
||||
}
|
142
src/api/get_files.rs
Normal file
142
src/api/get_files.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use actix_web::{get, web, Responder};
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::data::FileEntry;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FileRequest {
|
||||
mode: Option<DeliveryMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum DeliveryMode {
|
||||
Download,
|
||||
Browser,
|
||||
}
|
||||
|
||||
impl DeliveryMode {
|
||||
fn disposition(self) -> actix_web::http::header::DispositionType {
|
||||
match self {
|
||||
Self::Download => actix_web::http::header::DispositionType::Attachment,
|
||||
Self::Browser => actix_web::http::header::DispositionType::Inline,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_from_ext(ext: Option<&std::ffi::OsStr>) -> Self {
|
||||
let ext_str = ext.map(|x| x.to_str()).flatten();
|
||||
if let Some(ext) = ext_str {
|
||||
match ext {
|
||||
// images
|
||||
"png" | "jpg" => Self::Browser,
|
||||
// large video formats
|
||||
"mkv" | "avi" => Self::Download,
|
||||
// small video formats
|
||||
"3gp" | "mp4" => Self::Browser,
|
||||
_ => Self::Browser, // let browser figure it out
|
||||
}
|
||||
} else {
|
||||
Self::Browser // let browser decide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/file/{path:.*}")]
|
||||
pub async fn file(path: web::Path<String>, auth: BasicAuth, query: web::Query<FileRequest>) -> std::io::Result<impl Responder> {
|
||||
let args = super::CliArgs::get();
|
||||
if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed"))
|
||||
}
|
||||
let root = args.dir.unwrap();
|
||||
let filepath = root.join(&*path);
|
||||
let req_disposition = query.mode.unwrap_or_else(
|
||||
|| DeliveryMode::default_from_ext(filepath.extension())
|
||||
).disposition();
|
||||
println!("file PATH: {}", filepath.display());
|
||||
Ok(
|
||||
actix_files::NamedFile::open_async(&filepath).await?
|
||||
.prefer_utf8(false)
|
||||
.set_content_disposition(
|
||||
actix_web::http::header::ContentDisposition {
|
||||
disposition: req_disposition,
|
||||
parameters: vec![
|
||||
/*actix_web::http::header::DispositionParam::FilenameExt(
|
||||
actix_web::http::header::ExtendedValue {
|
||||
charset: actix_web::http::header::Charset::Iso_8859_1,
|
||||
language_tag: None,
|
||||
value: filepath.file_name()
|
||||
.map(
|
||||
|name| name
|
||||
.to_string_lossy()
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
).unwrap_or_else(|| Vec::with_capacity(0)),
|
||||
}
|
||||
),*/
|
||||
actix_web::http::header::DispositionParam::Filename(
|
||||
filepath.file_name()
|
||||
.map(
|
||||
|name| name
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
).unwrap_or_else(|| "unknown".into()),
|
||||
)
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/api/listdir/{path:.*}")]
|
||||
pub async fn dir(path: web::Path<String>, auth: BasicAuth) -> std::io::Result<impl Responder> {
|
||||
let args = super::CliArgs::get();
|
||||
if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed"))
|
||||
}
|
||||
let root = args.dir.unwrap();
|
||||
let domain = args.domain;
|
||||
let filepath = root.join(&*path);
|
||||
let scheme = if args.ssl {"https"} else {"http"};
|
||||
println!("dir PATH: {}", filepath.display());
|
||||
let mut entries = Vec::new();
|
||||
if !(path.is_empty() || &*path == "/") && filepath.parent().is_some() {
|
||||
let external_path = filepath.parent().expect("Path has no parent")
|
||||
.strip_prefix(&root).expect("path does not start with root path!").to_path_buf();
|
||||
let link = format!("{}://{}/api/listdir/{}", scheme, domain, external_path.to_string_lossy());
|
||||
entries.push(FileEntry {
|
||||
name: "..".to_owned(),
|
||||
path: external_path,
|
||||
is_dir: true,
|
||||
url: url_escape::encode_path(&link).to_owned().to_string(),
|
||||
});
|
||||
}
|
||||
//let pseudo_root = std::path::PathBuf::from("/");
|
||||
// build dir entry json
|
||||
for e in filepath.read_dir()? {
|
||||
let entry = e?;
|
||||
let external_path = entry.path().strip_prefix(&root).expect("path does not start with root path!").to_path_buf();
|
||||
let is_folder = entry.metadata()?.is_dir();
|
||||
let link = if is_folder {
|
||||
format!("{}://{}/api/listdir/{}", scheme, domain, external_path.to_string_lossy())
|
||||
} else {
|
||||
format!("{}://{}/api/file/{}", scheme, domain, external_path.to_string_lossy())
|
||||
};
|
||||
let external_name = entry.file_name().to_string_lossy().into_owned();
|
||||
if args.hidden || !external_name.starts_with('.') { // skip hidden by default
|
||||
entries.push(
|
||||
FileEntry {
|
||||
name: external_name,
|
||||
path: external_path,
|
||||
is_dir: is_folder,
|
||||
url: url_escape::encode_path(&link).to_owned().to_string(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// sort alphabetically
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(
|
||||
web::Json(entries)
|
||||
)
|
||||
}
|
52
src/api/get_index.rs
Normal file
52
src/api/get_index.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use actix_web::{get, web, Responder, HttpResponse};
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{self, Stream, StreamExt};
|
||||
|
||||
type BoxedError = Box<dyn std::error::Error + 'static>;
|
||||
|
||||
pub struct IndexPage {
|
||||
before: String,
|
||||
after: String,
|
||||
}
|
||||
|
||||
impl IndexPage {
|
||||
async fn render(&self) -> impl Stream<Item = Result<Bytes, BoxedError>> + Send {
|
||||
let renderer = yew::ServerRenderer::<crate::ui::App>::new();
|
||||
let before = self.before.clone();
|
||||
let after = self.after.clone();
|
||||
|
||||
stream::once(async move { before })
|
||||
.chain(renderer.render_stream())
|
||||
.chain(stream::once(async move { after }))
|
||||
.map(|m| Result::<_, BoxedError>::Ok(m.into()))
|
||||
}
|
||||
|
||||
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
|
||||
let index_html = std::fs::read_to_string(path.as_ref())?;
|
||||
let (index_before, index_after) = index_html.split_once("<body>").unwrap();
|
||||
let (mut index_before, index_after) = (index_before.to_owned(), index_after.to_owned());
|
||||
index_before.push_str("<body>");
|
||||
Ok(Self {
|
||||
before: index_before,
|
||||
after: index_after,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index_auth(page: web::Data<IndexPage>, auth: BasicAuth) -> std::io::Result<impl Responder> {
|
||||
let args = super::CliArgs::get();
|
||||
if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed"))
|
||||
}
|
||||
Ok(HttpResponse::Ok()
|
||||
.streaming(page.render().await))
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index_no_auth(page: web::Data<IndexPage>) -> std::io::Result<impl Responder> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.streaming(page.render().await))
|
||||
}
|
8
src/api/get_resources.rs
Normal file
8
src/api/get_resources.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use actix_web::{get, web, Responder};
|
||||
|
||||
#[get("/{name}")]
|
||||
pub async fn resource(path: web::Path<String>) -> impl Responder {
|
||||
//println!("GET resource {}", path);
|
||||
let filepath = std::path::PathBuf::from("dist").join(&*path);
|
||||
actix_files::NamedFile::open_async(filepath).await
|
||||
}
|
13
src/api/mod.rs
Normal file
13
src/api/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod get_args;
|
||||
mod get_banner;
|
||||
mod get_files;
|
||||
mod get_index;
|
||||
mod get_resources;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use get_args::CliArgs;
|
||||
pub use get_banner::bruce;
|
||||
pub use get_files::{dir, file};
|
||||
pub use get_index::{index_auth, index_no_auth, IndexPage};
|
||||
pub use get_resources::resource;
|
48
src/bin/backend.rs
Normal file
48
src/bin/backend.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use actix_web::{web, App, HttpServer};
|
||||
use actix_web_httpauth::extractors::basic;
|
||||
|
||||
use yarrr::api::{
|
||||
index_auth, index_no_auth, IndexPage,
|
||||
resource,
|
||||
dir, file,
|
||||
bruce,
|
||||
};
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let args = yarrr::api::CliArgs::get();
|
||||
|
||||
println!("cli: {:?}", args);
|
||||
|
||||
if args.dir.is_some() {
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap()))
|
||||
.app_data(basic::Config::default().realm("Restricted area"))
|
||||
.service(index_auth)
|
||||
.service(bruce)
|
||||
.service(resource)
|
||||
.service(dir)
|
||||
.service(file)
|
||||
})
|
||||
.bind(("127.0.0.1", 7534))?
|
||||
.run()
|
||||
.await
|
||||
} else {
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap()))
|
||||
.service(index_no_auth)
|
||||
.service(bruce)
|
||||
.service(resource)
|
||||
.service(dir)
|
||||
.service(file)
|
||||
})
|
||||
.bind(("127.0.0.1", 7534))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
}
|
11
src/bin/frontend.rs
Normal file
11
src/bin/frontend.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use yarrr::ui::App;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().hydrate();
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
compile_error!("frontend is for browsers (wasm32)");
|
||||
}
|
9
src/data/file_entry.rs
Normal file
9
src/data/file_entry.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct FileEntry {
|
||||
pub name: String,
|
||||
pub path: std::path::PathBuf,
|
||||
pub is_dir: bool,
|
||||
pub url: String,
|
||||
}
|
3
src/data/mod.rs
Normal file
3
src/data/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod file_entry;
|
||||
|
||||
pub use file_entry::FileEntry;
|
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod api;
|
||||
pub mod data;
|
||||
pub mod ui;
|
10
src/ui/app.rs
Normal file
10
src/ui/app.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
#[function_component(App)]
|
||||
pub fn app() -> Html {
|
||||
html! {
|
||||
<Suspense fallback={html!{"..."}}>
|
||||
<super::Landing />
|
||||
</Suspense>
|
||||
}
|
||||
}
|
151
src/ui/dir/browser.rs
Normal file
151
src/ui/dir/browser.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response};*/
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::data::FileEntry;
|
||||
|
||||
type FileEntryVec = Vec<FileEntry>;
|
||||
|
||||
async fn listdir(scheme: &str, domain: &str, path: &str) -> reqwest::Result<FileEntryVec> {
|
||||
let url = format!("{}://{}/api/listdir/{}", scheme, domain, path.trim_start_matches("/"));
|
||||
reqwest::get(&url)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
/// The possible states a fetch request can be in.
|
||||
pub enum FetchState<T> {
|
||||
NotFetching,
|
||||
Fetching,
|
||||
Success(T),
|
||||
Failed(reqwest::Error),
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
SetLoadState(FetchState<FileEntryVec>),
|
||||
Load,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct Props {
|
||||
pub scheme: String,
|
||||
pub domain: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct FileExplorer {
|
||||
files: FetchState<FileEntryVec>,
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
impl Component for FileExplorer {
|
||||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
files: FetchState::NotFetching,
|
||||
cwd: "???".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::SetLoadState(fetch_state) => {
|
||||
self.files = fetch_state;
|
||||
true
|
||||
}
|
||||
Msg::Load => {
|
||||
let props = ctx.props().clone();
|
||||
self.cwd = props.path.clone();
|
||||
ctx.link().send_future(async move {
|
||||
match listdir(&props.scheme, &props.domain, &props.path).await {
|
||||
Ok(md) => Msg::SetLoadState(FetchState::Success(md)),
|
||||
Err(err) => Msg::SetLoadState(FetchState::Failed(err)),
|
||||
}
|
||||
});
|
||||
ctx.link()
|
||||
.send_message(Msg::SetLoadState(FetchState::Fetching));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let icon_dim = "1em".to_owned();
|
||||
match &self.files {
|
||||
FetchState::NotFetching => {
|
||||
ctx.link().send_message(Msg::Load);
|
||||
html! {
|
||||
<div class={classes!("yarrr-files-browser")}>
|
||||
<div class={classes!("yarrr-files-browser-header")}>
|
||||
{ format!("Loading /{}", ctx.props().path) }
|
||||
{" "}
|
||||
<button onclick={ctx.link().callback(|_| Msg::Load)} class={classes!("yarrr-files-browser-header-refresh")}>
|
||||
<yew_icons::Icon icon_id={yew_icons::IconId::FeatherRefreshCcw} width={icon_dim.clone()} height={icon_dim.clone()}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class={classes!("yarrr-files-browser-entries")}>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
},
|
||||
FetchState::Fetching => html! {
|
||||
<div class={classes!("yarrr-files-browser")}>
|
||||
<div class={classes!("yarrr-files-browser-header")}>
|
||||
{ format!("Loading /{}", ctx.props().path) }
|
||||
{" "}
|
||||
<button onclick={ctx.link().callback(|_| Msg::Load)} class={classes!("yarrr-files-browser-header-refresh")}>
|
||||
<yew_icons::Icon icon_id={yew_icons::IconId::FeatherRefreshCcw} width={icon_dim.clone()} height={icon_dim.clone()}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class={classes!("yarrr-files-browser-entries")}>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
FetchState::Success(data) => {
|
||||
if self.cwd != ctx.props().path {
|
||||
ctx.link().send_message(Msg::Load);
|
||||
}
|
||||
html! {
|
||||
<div class={classes!("yarrr-files-browser")}>
|
||||
<div class={classes!("yarrr-files-browser-header")}>
|
||||
{ format!("/{} ({} entries)", ctx.props().path, data.len()) }
|
||||
{" "}
|
||||
<button onclick={ctx.link().callback(|_| Msg::Load)} class={classes!("yarrr-files-browser-header-refresh")}>
|
||||
<yew_icons::Icon icon_id={yew_icons::IconId::FeatherRefreshCcw} width={icon_dim.clone()} height={icon_dim.clone()}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class={classes!("yarrr-files-browser-entries")}>
|
||||
{ for data.iter().map(build_entry) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
},
|
||||
FetchState::Failed(err) => html! {
|
||||
<div class={classes!("yarrr-files-browser")}>
|
||||
<div class={classes!("yarrr-files-browser-header")}>
|
||||
{ format!("Failed to load /{}", ctx.props().path) }
|
||||
<br/>
|
||||
{ err }
|
||||
<br/>
|
||||
<button onclick={ctx.link().callback(|_| Msg::Load)} class={classes!("yarrr-files-browser-header-refresh")}>
|
||||
<yew_icons::Icon icon_id={yew_icons::IconId::FeatherRefreshCcw} width={icon_dim.clone()} height={icon_dim.clone()}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class={classes!("yarrr-files-browser-entries")}>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_entry(entry: &FileEntry) -> Html {
|
||||
html! {
|
||||
<super::DirEntry entry={entry.to_owned()}/>
|
||||
}
|
||||
}
|
70
src/ui/dir/entry.rs
Normal file
70
src/ui/dir/entry.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use yew::prelude::*;
|
||||
use yew_icons::{Icon, IconId};
|
||||
|
||||
use crate::data::FileEntry;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct Props {
|
||||
pub entry: FileEntry,
|
||||
}
|
||||
|
||||
#[function_component(DirEntry)]
|
||||
pub fn entry(props: &Props) -> Html {
|
||||
let fs_ctx: super::FilesystemContext = use_context().expect("No FilesystemContext");
|
||||
let s_ctx: std::rc::Rc<super::super::ServerCtx> = use_context().expect("No ServerCtx");
|
||||
let icon_dimension = "2em".to_owned();
|
||||
let inner = if props.entry.is_dir {
|
||||
let path = props.entry.path.clone();
|
||||
let icon = if props.entry.name == ".." {
|
||||
html! { <Icon icon_id={IconId::FeatherCornerLeftUp} width={icon_dimension.clone()} height={icon_dimension.clone()}/> }
|
||||
} else {
|
||||
html! { <Icon icon_id={IconId::FeatherFolder} width={icon_dimension.clone()} height={icon_dimension.clone()}/> }
|
||||
};
|
||||
html! {
|
||||
<a href={"#"} class={classes!("yarrr-file-entry-link")} onclick={
|
||||
move |_| fs_ctx.dispatch(super::FilesystemCtxAction::NavigateTo(path.clone()))
|
||||
}>
|
||||
<div class={classes!("yarrr-file-entry-dir")}>
|
||||
<div class={classes!("yarrr-file-entry-icon")}>
|
||||
{icon}
|
||||
</div>
|
||||
<div class={classes!("yarrr-file-entry-name")}>
|
||||
{&props.entry.name}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
} else {
|
||||
let download_url = format!("{}://{}/api/file/{}",
|
||||
if s_ctx.ssl { "https" } else { "http" },
|
||||
s_ctx.domain,
|
||||
props.entry.path.to_string_lossy());
|
||||
let icon = if props.entry.name.ends_with(".mkv") {
|
||||
html! { <Icon icon_id={IconId::FeatherFilm}
|
||||
width={icon_dimension.clone()} height={icon_dimension.clone()}/> }
|
||||
} else if props.entry.name.ends_with(".png") || props.entry.name.ends_with(".jpg") {
|
||||
html! { <Icon icon_id={IconId::FeatherImage}
|
||||
width={icon_dimension.clone()} height={icon_dimension.clone()}/> }
|
||||
} else {
|
||||
html! { <Icon icon_id={IconId::FeatherFile}
|
||||
width={icon_dimension.clone()} height={icon_dimension.clone()} /> }
|
||||
};
|
||||
html! {
|
||||
<a href={download_url} class={classes!("yarrr-file-entry-link")}>
|
||||
<div class={classes!("yarrr-file-entry-file")}>
|
||||
<div class={classes!("yarrr-file-entry-icon")}>
|
||||
{icon}
|
||||
</div>
|
||||
<div class={classes!("yarrr-file-entry-name")}>
|
||||
{&props.entry.name}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
};
|
||||
html! {
|
||||
<div class={classes!("yarrr-file-entry")}>
|
||||
{inner}
|
||||
</div>
|
||||
}
|
||||
}
|
33
src/ui/dir/fs_ctx.rs
Normal file
33
src/ui/dir/fs_ctx.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use std::rc::Rc;
|
||||
use std::path::PathBuf;
|
||||
use yew::prelude::*;
|
||||
|
||||
pub enum FilesystemCtxAction {
|
||||
NavigateTo(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct FilesystemCtx {
|
||||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
pub type FilesystemContext = UseReducerHandle<FilesystemCtx>;
|
||||
|
||||
impl FilesystemCtx {
|
||||
pub fn init() -> Self {
|
||||
Self {
|
||||
cwd: PathBuf::from(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for FilesystemCtx {
|
||||
type Action = FilesystemCtxAction;
|
||||
|
||||
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
|
||||
let cwd_new = match action {
|
||||
FilesystemCtxAction::NavigateTo(path) => path,
|
||||
};
|
||||
Rc::new(Self { cwd: cwd_new })
|
||||
}
|
||||
}
|
7
src/ui/dir/mod.rs
Normal file
7
src/ui/dir/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod browser;
|
||||
mod entry;
|
||||
mod fs_ctx;
|
||||
|
||||
pub use browser::FileExplorer;
|
||||
pub use entry::DirEntry;
|
||||
pub use fs_ctx::{FilesystemContext, FilesystemCtx, FilesystemCtxAction};
|
34
src/ui/landing.rs
Normal file
34
src/ui/landing.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use yew::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
||||
#[function_component(Landing)]
|
||||
pub fn landing() -> HtmlResult {
|
||||
let server_ctx = use_prepared_state!(
|
||||
async move |_| -> super::ServerCtx { super::build_server_ctx().await }, ()
|
||||
)?.expect("Missing server-provided context");
|
||||
|
||||
let fs_ctx = use_reducer(super::dir::FilesystemCtx::init);
|
||||
|
||||
if server_ctx.root_dir.is_some() {
|
||||
let scheme = if server_ctx.ssl {"https"} else {"http"}.to_owned();
|
||||
let domain = server_ctx.domain.clone();
|
||||
let path = fs_ctx.cwd.to_string_lossy().to_string();
|
||||
Ok(html! {
|
||||
<ContextProvider<Rc<super::ServerCtx>> context={server_ctx}>
|
||||
<img src="/banner.png" class={classes!("yarrr-proof-of-purchase")}/>
|
||||
<ContextProvider<super::dir::FilesystemContext> context={fs_ctx}>
|
||||
<super::dir::FileExplorer {scheme} {domain} {path}/>
|
||||
</ContextProvider<super::dir::FilesystemContext>>
|
||||
</ContextProvider<Rc<super::ServerCtx>>>
|
||||
})
|
||||
} else {
|
||||
Ok(html! {
|
||||
<Suspense fallback={html!{"..."}}>
|
||||
//<img src="/banner.png" class={classes!("yarrr-proof-of-purchase")}/>
|
||||
<super::Rant />
|
||||
//<h1>{ "Hello World!" }</h1>
|
||||
</Suspense>
|
||||
})
|
||||
}
|
||||
}
|
12
src/ui/mod.rs
Normal file
12
src/ui/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
mod app;
|
||||
pub mod dir;
|
||||
mod landing;
|
||||
mod rant;
|
||||
mod server_ctx;
|
||||
|
||||
pub use app::App;
|
||||
pub use landing::Landing;
|
||||
pub use rant::Rant;
|
||||
pub use server_ctx::ServerCtx;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use server_ctx::build_server_ctx;
|
78
src/ui/rant.rs
Normal file
78
src/ui/rant.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
#[function_component(Rant)]
|
||||
pub fn render() -> Html {
|
||||
html! {
|
||||
<div class={classes!("yarrr-rant")}>
|
||||
<p class={classes!("yarrr-rant-quote")}>
|
||||
{
|
||||
"One thing that we have learned is that piracy is not a pricing issue. It’s a service issue"
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-quote-attrib")}>
|
||||
{
|
||||
"Gabe Newell (CEO of Valve, creator of the Steam video game online store)"
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
So why do most media copyright holders offer poor service and then complain about piracy?
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
Currently, the best legal way to purchase a TV show or movie is to buy a physical disc.
|
||||
The best legal way to stream a TV show or movie is to search through all streaming services and hope one of them has it.
|
||||
If no service has it, the best legal way is to purchase a physical disc, copy it onto a computer and then stream it from there.
|
||||
That's ridiculous.
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
It's 2023 and most people have fast Internet.
|
||||
We should be able to buy a TV show or movie online and store it however we want.
|
||||
We should be able to go to one streaming service and find the TV show or movie we want.
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
One piracy site has the same content as all streaming services combined, for free.
|
||||
Most piracy sites have even more TV shows and movies than the paid services.
|
||||
Better selection, for free and all in one spot.
|
||||
Who wouldn't want to use that?
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
Media companies, please stop crying to the government about self-inflicted issues.
|
||||
Make a good service and we'll stop pirating.
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
Not sure how to make a good service?
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
<p class={classes!("yarrr-rant-p")}>
|
||||
{
|
||||
r#"
|
||||
Try making one which benefits the consumer: no region locks, no DRM, no disappearing shows.
|
||||
Take inspiration from popular piracy sites, because they are your competition.
|
||||
"#
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
18
src/ui/server_ctx.rs
Normal file
18
src/ui/server_ctx.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct ServerCtx {
|
||||
pub domain: String,
|
||||
pub root_dir: Option<std::path::PathBuf>,
|
||||
pub ssl: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn build_server_ctx() -> ServerCtx {
|
||||
let args = crate::api::CliArgs::get();
|
||||
ServerCtx {
|
||||
domain: args.domain,
|
||||
root_dir: args.dir,
|
||||
ssl: args.ssl,
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue