Compare commits
No commits in common. "main" and "v0.2.0" have entirely different histories.
32 changed files with 682 additions and 1069 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1 @@
|
||||||
/target
|
/target
|
||||||
*.snap.new
|
|
||||||
*.pending-snap
|
|
||||||
|
|
|
@ -18,5 +18,4 @@ repos:
|
||||||
name: ui/menu lint+fmt
|
name: ui/menu lint+fmt
|
||||||
language: system
|
language: system
|
||||||
files: ^ui/menu/
|
files: ^ui/menu/
|
||||||
pass_filenames: false
|
entry: sh -c "npm run --prefix ui/menu pc"
|
||||||
entry: npm run --prefix ui/menu pc
|
|
||||||
|
|
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -2,59 +2,6 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [0.4.2] - 2023-07-22
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Container entrypoint
|
|
||||||
- Website version ordering
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Fix npm pre-commit hook
|
|
||||||
|
|
||||||
## [0.4.1] - 2023-04-05
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Stop propagation of key events on menu search
|
|
||||||
- Use sh
|
|
||||||
- Remove version prefix from "latest" tag
|
|
||||||
- Detect CI commit SHA
|
|
||||||
- Use better abbreviations for page names
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Add upload script
|
|
||||||
|
|
||||||
## [0.4.0] - 2023-04-02
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- [**breaking**] Switch database format to CBOR (not compatible with previous format)
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Hide version-bump messages
|
|
||||||
|
|
||||||
## [0.3.0] - 2023-04-02
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Move menu bar down
|
|
||||||
- Set icon size to 48px
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Add last-modified date to all GET responses
|
|
||||||
- Add last-version tag to PageInfoModal
|
|
||||||
- Purge file storage daily
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Update cargo dependencies
|
|
||||||
- Bump version -> 0.3.0
|
|
||||||
|
|
||||||
## [0.2.0] - 2023-04-01
|
## [0.2.0] - 2023-04-01
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
1248
Cargo.lock
generated
1248
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "talon"
|
name = "talon"
|
||||||
version = "0.4.2"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["ThetaDev <t.testboy@gmail.com>"]
|
authors = ["ThetaDev <t.testboy@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -16,7 +16,7 @@ tokio = { version = "1.25.0", features = ["rt-multi-thread", "fs", "signal"] }
|
||||||
sled = "0.34.7"
|
sled = "0.34.7"
|
||||||
serde = "1.0.152"
|
serde = "1.0.152"
|
||||||
serde_json = "1.0.93"
|
serde_json = "1.0.93"
|
||||||
serde_cbor = "0.11.2"
|
rmp-serde = "1.1.1"
|
||||||
toml = "0.7.2"
|
toml = "0.7.2"
|
||||||
thiserror = "1.0.38"
|
thiserror = "1.0.38"
|
||||||
time = { version = "0.3.15", features = [
|
time = { version = "0.3.15", features = [
|
||||||
|
@ -27,7 +27,7 @@ time = { version = "0.3.15", features = [
|
||||||
httpdate = "1.0.2"
|
httpdate = "1.0.2"
|
||||||
sha2 = "0.10.6"
|
sha2 = "0.10.6"
|
||||||
path_macro = "1.0.0"
|
path_macro = "1.0.0"
|
||||||
hex-literal = "0.4.0"
|
hex-literal = "0.3.4"
|
||||||
hex = { version = "0.4.3", features = ["serde"] }
|
hex = { version = "0.4.3", features = ["serde"] }
|
||||||
temp-dir = "0.1.11"
|
temp-dir = "0.1.11"
|
||||||
zip = { version = "0.6.4", default-features = false, features = [
|
zip = { version = "0.6.4", default-features = false, features = [
|
||||||
|
@ -54,7 +54,6 @@ shadow-rs = "0.21.0"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
rust-embed = { version = "6.6.1", features = ["poem-ex"] }
|
rust-embed = { version = "6.6.1", features = ["poem-ex"] }
|
||||||
image = "0.24.6"
|
image = "0.24.6"
|
||||||
clokwerk = { version = "0.4.0", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.17.0"
|
rstest = "0.17.0"
|
||||||
|
|
|
@ -48,7 +48,7 @@ commit_parsers = [
|
||||||
{ message = "^refactor", group = "Refactor"},
|
{ message = "^refactor", group = "Refactor"},
|
||||||
{ message = "^style", group = "Styling"},
|
{ message = "^style", group = "Styling"},
|
||||||
{ message = "^test", group = "Testing"},
|
{ message = "^test", group = "Testing"},
|
||||||
{ message = "^chore\\(release\\):", skip = true},
|
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||||
{ message = "(^chore)|(^ci)", group = "Miscellaneous Tasks"},
|
{ message = "(^chore)|(^ci)", group = "Miscellaneous Tasks"},
|
||||||
{ body = ".*security", group = "Security"},
|
{ body = ".*security", group = "Security"},
|
||||||
]
|
]
|
||||||
|
|
|
@ -41,8 +41,7 @@ for arch in "${ARCHITECTURES[@]}"; do
|
||||||
|
|
||||||
# Finalize container
|
# Finalize container
|
||||||
buildah umount "$container"
|
buildah umount "$container"
|
||||||
# entrypoint syntax: see issue https://github.com/containers/buildah/issues/1768
|
buildah config --entrypoint "/talon" --cmd "run -d /data" --arch "$arch" --port 3000 --author "ThetaDev" "$container"
|
||||||
buildah config --entrypoint '["/talon"]' --cmd "run -d /data" --arch "$arch" --port 3000 --author "ThetaDev" "$container"
|
|
||||||
buildah commit "$container" "$IMAGE:$arch-$TAG"
|
buildah commit "$container" "$IMAGE:$arch-$TAG"
|
||||||
|
|
||||||
buildah manifest add "$REGISTRY/$IMAGE:$TAG" "$IMAGE:$arch-$TAG"
|
buildah manifest add "$REGISTRY/$IMAGE:$TAG" "$IMAGE:$arch-$TAG"
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Check for dependencies
|
|
||||||
which curl > /dev/null
|
|
||||||
which jq > /dev/null
|
|
||||||
|
|
||||||
# Assert required variables
|
|
||||||
if [ -z "$TALON_KEY" ]; then echo "TALON_KEY unset"; exit 1; fi
|
|
||||||
if [ -z "$TALON_URL" ]; then echo "TALON_URL unset"; exit 1; fi
|
|
||||||
if [ -z "$SUBDOMAIN" ]; then echo "SUBDOMAIN unset"; exit 1; fi
|
|
||||||
|
|
||||||
API_URL="$TALON_URL/api"
|
|
||||||
API_KEY_H="x-api-key: $TALON_KEY"
|
|
||||||
|
|
||||||
# Check if the website already exists
|
|
||||||
WEBSITE_STATUS=$(curl --head -o /dev/null -s -w "%{http_code}" "$API_URL/website/$SUBDOMAIN")
|
|
||||||
if [ "$WEBSITE_STATUS" = "200" ]; then
|
|
||||||
echo "Website '$SUBDOMAIN' found"
|
|
||||||
else
|
|
||||||
# Create the website if it does not exist
|
|
||||||
if [ -z "$WEBSITE_NAME" ]; then echo "WEBSITE_NAME unset"; exit 1; fi
|
|
||||||
|
|
||||||
CREATE_BODY=$(jq -c --null-input --arg name "$WEBSITE_NAME" --arg color "$WEBSITE_COLOR" \
|
|
||||||
--arg visibility "$WEBSITE_VISIBILITY" --arg source_url "$WEBSITE_SOURCE_URL" \
|
|
||||||
--arg source_icon "$WEBSITE_SOURCE_ICON" \
|
|
||||||
'{"name": $name, "color": $color, "visibility": $visibility, "source_url": $source_url, "source_icon": $source_icon} | delpaths([path(.[]| select(.==""))])')
|
|
||||||
echo "Creating website '$SUBDOMAIN': $CREATE_BODY"
|
|
||||||
|
|
||||||
curl -Ss --fail -X "PUT" -H "$API_KEY_H" -H "content-type: application/json" --data "$CREATE_BODY" "$API_URL/website/$SUBDOMAIN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check the upload directory
|
|
||||||
if [ ! -d "$1" ]; then echo "Upload directory does not exist"; exit 1; fi
|
|
||||||
if [ ! -f "$1/index.html" ]; then echo "Upload directory does not contain index.html"; exit 1; fi
|
|
||||||
|
|
||||||
# Validate fallback page param
|
|
||||||
if [ "$FALLBACK" ] && [ ! -f "$1/$FALLBACK" ]; then echo "fallback page $FALLBACK does not exist"; exit 1; fi
|
|
||||||
|
|
||||||
# Automatically detect fallback pages
|
|
||||||
if [ -z "$SPA" ] && [ -z "$FALLBACK" ]; then
|
|
||||||
if [ -f "$1/404.html" ]; then FALLBACK="404.html"; fi
|
|
||||||
if [ -f "$1/200.html" ]; then SPA=true; FALLBACK="200.html"; fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
push_arg() {
|
|
||||||
if [ "$UPLOAD_ARGS" ]; then UPLOAD_ARGS="$UPLOAD_ARGS&"; fi
|
|
||||||
UPLOAD_ARGS="$UPLOAD_ARGS$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$FALLBACK" ]; then push_arg "fallback=$FALLBACK"; fi
|
|
||||||
if [ "$SPA" = "true" ]; then push_arg "spa=true"; fi
|
|
||||||
|
|
||||||
if [ "$UPLOAD_ARGS" ]; then UPLOAD_ARGS="?$UPLOAD_ARGS"; fi
|
|
||||||
|
|
||||||
if [ "$CI_COMMIT_SHA" ]; then
|
|
||||||
echo "Git commit: $CI_COMMIT_SHA"
|
|
||||||
push_arg "commit=$CI_COMMIT_SHA"
|
|
||||||
elif GIT_COMMIT=$(git rev-parse HEAD 2> /dev/null); then
|
|
||||||
echo "Git commit: $GIT_COMMIT"
|
|
||||||
push_arg "commit=$GIT_COMMIT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compress website
|
|
||||||
ARCHIVE=$(mktemp)
|
|
||||||
tar -cz --directory "$1" --file "$ARCHIVE" .
|
|
||||||
|
|
||||||
# Upload website
|
|
||||||
echo "Version params: $UPLOAD_ARGS"
|
|
||||||
echo "Uploading..."
|
|
||||||
curl --fail -X "POST" -H "$API_KEY_H" -H "content-type: application/octet-stream" --data-binary "@$ARCHIVE" "$API_URL/website/$SUBDOMAIN/upload$UPLOAD_ARGS"
|
|
||||||
|
|
||||||
rm "$ARCHIVE"
|
|
||||||
|
|
||||||
echo "Website uploaded ;-)"
|
|
99
src/api.rs
99
src/api.rs
|
@ -63,8 +63,6 @@ pub enum ApiError {
|
||||||
InvalidArchiveType,
|
InvalidArchiveType,
|
||||||
#[error("invalid color")]
|
#[error("invalid color")]
|
||||||
InvalidColor,
|
InvalidColor,
|
||||||
#[error("join error: {0}")]
|
|
||||||
TokioJoin(#[from] tokio::task::JoinError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for ApiError {
|
impl ResponseError for ApiError {
|
||||||
|
@ -75,7 +73,6 @@ impl ResponseError for ApiError {
|
||||||
| ApiError::InvalidArchiveType
|
| ApiError::InvalidArchiveType
|
||||||
| ApiError::InvalidColor => StatusCode::BAD_REQUEST,
|
| ApiError::InvalidColor => StatusCode::BAD_REQUEST,
|
||||||
ApiError::NoAccess => StatusCode::FORBIDDEN,
|
ApiError::NoAccess => StatusCode::FORBIDDEN,
|
||||||
ApiError::TokioJoin(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,17 +99,11 @@ impl TalonApi {
|
||||||
&self,
|
&self,
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
) -> Result<Response<Json<Website>>> {
|
) -> Result<Json<Website>> {
|
||||||
talon
|
talon
|
||||||
.db
|
.db
|
||||||
.get_website(&subdomain)
|
.get_website(&subdomain)
|
||||||
.map(|website| {
|
.map(|w| Json(Website::from((subdomain.0, w))))
|
||||||
let modified = website.updated_at;
|
|
||||||
Response::new(Json(Website::from((subdomain.0, website)))).header(
|
|
||||||
header::LAST_MODIFIED,
|
|
||||||
httpdate::fmt_http_date(modified.into()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,14 +163,9 @@ impl TalonApi {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain, Access::Modify)?;
|
auth.check_subdomain(&subdomain, Access::Modify)?;
|
||||||
|
|
||||||
let t2 = talon.clone();
|
talon
|
||||||
let sd = subdomain.clone();
|
.icons
|
||||||
tokio::task::spawn_blocking(move || {
|
.insert_icon(Cursor::new(data.as_slice()), &subdomain)?;
|
||||||
t2.icons.insert_icon(Cursor::new(data.as_slice()), &sd)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(ApiError::from)??;
|
|
||||||
|
|
||||||
talon.db.update_website(
|
talon.db.update_website(
|
||||||
&subdomain,
|
&subdomain,
|
||||||
db::model::WebsiteUpdate {
|
db::model::WebsiteUpdate {
|
||||||
|
@ -266,8 +252,7 @@ impl TalonApi {
|
||||||
/// Mimimum visibility of the websites
|
/// Mimimum visibility of the websites
|
||||||
#[oai(default)]
|
#[oai(default)]
|
||||||
visibility: Query<Visibility>,
|
visibility: Query<Visibility>,
|
||||||
) -> Result<Response<Json<Vec<Website>>>> {
|
) -> Result<Json<Vec<Website>>> {
|
||||||
let modified = talon.db.websites_last_update()?;
|
|
||||||
talon
|
talon
|
||||||
.db
|
.db
|
||||||
.get_websites()
|
.get_websites()
|
||||||
|
@ -285,10 +270,7 @@ impl TalonApi {
|
||||||
Err(_) => true,
|
Err(_) => true,
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map(|data| {
|
.map(Json)
|
||||||
Response::new(Json(data))
|
|
||||||
.header(header::LAST_MODIFIED, httpdate::fmt_http_date(modified))
|
|
||||||
})
|
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,19 +280,15 @@ impl TalonApi {
|
||||||
&self,
|
&self,
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
) -> Result<Response<Json<Vec<Version>>>> {
|
) -> Result<Json<Vec<Version>>> {
|
||||||
let website = talon.db.get_website(&subdomain)?;
|
talon.db.website_exists(&subdomain)?;
|
||||||
let mut versions = talon
|
talon
|
||||||
.db
|
.db
|
||||||
.get_website_versions(&subdomain)
|
.get_website_versions(&subdomain)
|
||||||
.map(|r| r.map(Version::from))
|
.map(|r| r.map(Version::from))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()
|
||||||
versions.sort_by_key(|v| v.id);
|
.map(Json)
|
||||||
|
.map_err(Error::from)
|
||||||
Ok(Response::new(Json(versions)).header(
|
|
||||||
header::LAST_MODIFIED,
|
|
||||||
httpdate::fmt_http_date(website.updated_at.into()),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get version
|
/// Get version
|
||||||
|
@ -320,17 +298,11 @@ impl TalonApi {
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
version: Path<u32>,
|
version: Path<u32>,
|
||||||
) -> Result<Response<Json<Version>>> {
|
) -> Result<Json<Version>> {
|
||||||
talon
|
talon
|
||||||
.db
|
.db
|
||||||
.get_version(&subdomain, *version)
|
.get_version(&subdomain, *version)
|
||||||
.map(|v| {
|
.map(|v| Json(Version::from((*version, v))))
|
||||||
let create_date = v.created_at;
|
|
||||||
Response::new(Json(Version::from((*version, v)))).header(
|
|
||||||
header::LAST_MODIFIED,
|
|
||||||
httpdate::fmt_http_date(create_date.into()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,19 +313,14 @@ impl TalonApi {
|
||||||
talon: Data<&Talon>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
version: Path<u32>,
|
version: Path<u32>,
|
||||||
) -> Result<Response<Json<Vec<VersionFile>>>> {
|
) -> Result<Json<Vec<VersionFile>>> {
|
||||||
let v = talon.db.get_version(&subdomain, *version)?;
|
talon.db.version_exists(&subdomain, *version)?;
|
||||||
talon
|
talon
|
||||||
.db
|
.db
|
||||||
.get_version_files(&subdomain, *version)
|
.get_version_files(&subdomain, *version)
|
||||||
.map(|r| r.map(VersionFile::from))
|
.map(|r| r.map(VersionFile::from))
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map(|r| {
|
.map(Json)
|
||||||
Response::new(Json(r)).header(
|
|
||||||
header::LAST_MODIFIED,
|
|
||||||
httpdate::fmt_http_date(v.created_at.into()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,21 +380,15 @@ impl TalonApi {
|
||||||
|
|
||||||
// Try to store the uploaded website
|
// Try to store the uploaded website
|
||||||
// If this fails, the new version needs to be deleted
|
// If this fails, the new version needs to be deleted
|
||||||
fn try_insert(
|
let try_insert = || {
|
||||||
talon: &Talon,
|
|
||||||
data: Binary<Vec<u8>>,
|
|
||||||
subdomain: &str,
|
|
||||||
version: u32,
|
|
||||||
fallback: Option<String>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if data.starts_with(&hex!("1f8b")) {
|
if data.starts_with(&hex!("1f8b")) {
|
||||||
talon
|
talon
|
||||||
.storage
|
.storage
|
||||||
.insert_tgz_archive(data.as_slice(), subdomain, version)?;
|
.insert_tgz_archive(data.as_slice(), &subdomain, version)?;
|
||||||
} else if data.starts_with(&hex!("504b0304")) {
|
} else if data.starts_with(&hex!("504b0304")) {
|
||||||
talon.storage.insert_zip_archive(
|
talon.storage.insert_zip_archive(
|
||||||
Cursor::new(data.as_slice()),
|
Cursor::new(data.as_slice()),
|
||||||
subdomain,
|
&subdomain,
|
||||||
version,
|
version,
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -435,26 +396,20 @@ impl TalonApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validata fallback path
|
// Validata fallback path
|
||||||
if let Some(fallback) = &fallback {
|
if let Some(fallback) = &fallback.0 {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
talon
|
talon
|
||||||
.storage
|
.storage
|
||||||
.get_file(subdomain, version, fallback, &Default::default())
|
.get_file(&subdomain, version, fallback, &Default::default())
|
||||||
{
|
{
|
||||||
return Err(Error::from(ApiError::InvalidFallback(e.to_string())));
|
return Err(Error::from(ApiError::InvalidFallback(e.to_string())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
};
|
||||||
|
|
||||||
let t2 = talon.clone();
|
match try_insert() {
|
||||||
let sd = subdomain.clone();
|
Ok(()) => {
|
||||||
|
|
||||||
match tokio::task::spawn_blocking(move || try_insert(&t2, data, &sd, version, fallback.0))
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::from(ApiError::from(e)))
|
|
||||||
{
|
|
||||||
Ok(Ok(())) => {
|
|
||||||
talon.db.update_website(
|
talon.db.update_website(
|
||||||
&subdomain,
|
&subdomain,
|
||||||
db::model::WebsiteUpdate {
|
db::model::WebsiteUpdate {
|
||||||
|
@ -464,7 +419,7 @@ impl TalonApi {
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) | Ok(Err(e)) => {
|
Err(e) => {
|
||||||
// Remove the bad version and decrement the id counter
|
// Remove the bad version and decrement the id counter
|
||||||
let _ = talon.db.delete_version(&subdomain, version, false);
|
let _ = talon.db.delete_version(&subdomain, version, false);
|
||||||
let _ = talon.db.decrement_vid(&subdomain, version);
|
let _ = talon.db.decrement_vid(&subdomain, version);
|
||||||
|
|
|
@ -80,26 +80,10 @@ impl Config {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct ServerCfg {
|
pub struct ServerCfg {
|
||||||
/// Address to bind the server to
|
|
||||||
///
|
|
||||||
/// Default: `0.0.0.0:3000`
|
|
||||||
pub address: String,
|
pub address: String,
|
||||||
/// Root domain (and port if non-standard) under which Talon is available
|
|
||||||
///
|
|
||||||
/// Default: `localhost:3000`
|
|
||||||
pub root_domain: String,
|
pub root_domain: String,
|
||||||
/// Subdomain used for Talon internals (API, assets)
|
|
||||||
///
|
|
||||||
/// Default: `talon`
|
|
||||||
pub internal_subdomain: String,
|
pub internal_subdomain: String,
|
||||||
/// URL under which the internals are available
|
|
||||||
///
|
|
||||||
/// Default: `http://talon.localhost:3000`
|
|
||||||
pub internal_url: String,
|
pub internal_url: String,
|
||||||
/// Interval in minutes between file storage purges
|
|
||||||
///
|
|
||||||
/// Default: 1440 (24h)
|
|
||||||
pub purge_interval: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerCfg {
|
impl Default for ServerCfg {
|
||||||
|
@ -109,7 +93,6 @@ impl Default for ServerCfg {
|
||||||
root_domain: "localhost:3000".to_owned(),
|
root_domain: "localhost:3000".to_owned(),
|
||||||
internal_subdomain: "talon".to_owned(),
|
internal_subdomain: "talon".to_owned(),
|
||||||
internal_url: "http://talon.localhost:3000".to_owned(),
|
internal_url: "http://talon.localhost:3000".to_owned(),
|
||||||
purge_interval: 1440,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,10 @@ struct DbInner {
|
||||||
pub enum DbError {
|
pub enum DbError {
|
||||||
#[error("sled db error: {0}")]
|
#[error("sled db error: {0}")]
|
||||||
Sled(#[from] sled::Error),
|
Sled(#[from] sled::Error),
|
||||||
#[error("cbor serialization error: {0}")]
|
#[error("msgpack serialization error: {0}")]
|
||||||
Serialize(#[from] serde_cbor::Error),
|
Serialize(#[from] rmp_serde::encode::Error),
|
||||||
|
#[error("msgpack deserialization error: {0}")]
|
||||||
|
Deserialize(#[from] rmp_serde::decode::Error),
|
||||||
#[error("json serialization error: {0}")]
|
#[error("json serialization error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
#[error("{0} with id `{1}` already exists")]
|
#[error("{0} with id `{1}` already exists")]
|
||||||
|
@ -98,7 +100,7 @@ impl Db {
|
||||||
for item in self.i.websites.iter() {
|
for item in self.i.websites.iter() {
|
||||||
let (k, v) = item?;
|
let (k, v) = item?;
|
||||||
let key = Self::key_to_string(k.to_vec())?;
|
let key = Self::key_to_string(k.to_vec())?;
|
||||||
let value = serde_cbor::from_slice::<Website>(&v)?;
|
let value = rmp_serde::from_slice::<Website>(&v)?;
|
||||||
|
|
||||||
let dataset = ExportDataset::Website { key, value };
|
let dataset = ExportDataset::Website { key, value };
|
||||||
|
|
||||||
|
@ -109,7 +111,7 @@ impl Db {
|
||||||
for item in self.i.versions.iter() {
|
for item in self.i.versions.iter() {
|
||||||
let (k, v) = item?;
|
let (k, v) = item?;
|
||||||
let key = Self::key_to_string(k.to_vec())?;
|
let key = Self::key_to_string(k.to_vec())?;
|
||||||
let value = serde_cbor::from_slice::<Version>(&v)?;
|
let value = rmp_serde::from_slice::<Version>(&v)?;
|
||||||
|
|
||||||
let dataset = ExportDataset::Version { key, value };
|
let dataset = ExportDataset::Version { key, value };
|
||||||
serde_json::to_writer(&mut writer, &dataset)?;
|
serde_json::to_writer(&mut writer, &dataset)?;
|
||||||
|
@ -150,11 +152,11 @@ impl Db {
|
||||||
fn import_dataset(&self, ds: ExportDataset) -> Result<()> {
|
fn import_dataset(&self, ds: ExportDataset) -> Result<()> {
|
||||||
match ds {
|
match ds {
|
||||||
ExportDataset::Website { key, value } => {
|
ExportDataset::Website { key, value } => {
|
||||||
let data = serde_cbor::to_vec(&value)?;
|
let data = rmp_serde::to_vec(&value)?;
|
||||||
self.i.websites.insert(key, data)?;
|
self.i.websites.insert(key, data)?;
|
||||||
}
|
}
|
||||||
ExportDataset::Version { key, value } => {
|
ExportDataset::Version { key, value } => {
|
||||||
let data = serde_cbor::to_vec(&value)?;
|
let data = rmp_serde::to_vec(&value)?;
|
||||||
self.i.versions.insert(key, data)?;
|
self.i.versions.insert(key, data)?;
|
||||||
}
|
}
|
||||||
ExportDataset::File { key, value } => {
|
ExportDataset::File { key, value } => {
|
||||||
|
@ -176,13 +178,13 @@ impl Db {
|
||||||
/// Get a website from the database
|
/// Get a website from the database
|
||||||
pub fn get_website(&self, subdomain: &str) -> Result<Website> {
|
pub fn get_website(&self, subdomain: &str) -> Result<Website> {
|
||||||
let data = self.i.websites.get(subdomain)?;
|
let data = self.i.websites.get(subdomain)?;
|
||||||
data.and_then(|data| serde_cbor::from_slice::<Website>(data.as_ref()).ok())
|
data.and_then(|data| rmp_serde::from_slice::<Website>(data.as_ref()).ok())
|
||||||
.ok_or_else(|| DbError::NotExists("website", subdomain.to_owned()))
|
.ok_or_else(|| DbError::NotExists("website", subdomain.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a new website into the database
|
/// Insert a new website into the database
|
||||||
pub fn insert_website(&self, subdomain: &str, website: &Website) -> Result<()> {
|
pub fn insert_website(&self, subdomain: &str, website: &Website) -> Result<()> {
|
||||||
let data = serde_cbor::to_vec(website)?;
|
let data = rmp_serde::to_vec(website)?;
|
||||||
self.i
|
self.i
|
||||||
.websites
|
.websites
|
||||||
.compare_and_swap(subdomain, None::<&[u8]>, Some(data))?
|
.compare_and_swap(subdomain, None::<&[u8]>, Some(data))?
|
||||||
|
@ -196,7 +198,7 @@ impl Db {
|
||||||
.i
|
.i
|
||||||
.websites
|
.websites
|
||||||
.update_and_fetch(subdomain, |data| match data {
|
.update_and_fetch(subdomain, |data| match data {
|
||||||
Some(data) => match serde_cbor::from_slice::<Website>(data) {
|
Some(data) => match rmp_serde::from_slice::<Website>(data) {
|
||||||
Ok(mut w) => {
|
Ok(mut w) => {
|
||||||
let website = website.clone();
|
let website = website.clone();
|
||||||
w.name = website.name.unwrap_or(w.name);
|
w.name = website.name.unwrap_or(w.name);
|
||||||
|
@ -206,9 +208,8 @@ impl Db {
|
||||||
w.source_url = website.source_url.unwrap_or(w.source_url);
|
w.source_url = website.source_url.unwrap_or(w.source_url);
|
||||||
w.source_icon = website.source_icon.unwrap_or(w.source_icon);
|
w.source_icon = website.source_icon.unwrap_or(w.source_icon);
|
||||||
w.has_icon = website.has_icon.unwrap_or(w.has_icon);
|
w.has_icon = website.has_icon.unwrap_or(w.has_icon);
|
||||||
w.updated_at = OffsetDateTime::now_utc();
|
|
||||||
|
|
||||||
serde_cbor::to_vec(&w).ok()
|
rmp_serde::to_vec(&w).ok()
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
|
@ -250,7 +251,7 @@ impl Db {
|
||||||
self.i.websites.iter().map(|r| {
|
self.i.websites.iter().map(|r| {
|
||||||
r.map_err(DbError::from).and_then(|(k, v)| {
|
r.map_err(DbError::from).and_then(|(k, v)| {
|
||||||
let subdomain = Self::key_to_string(k.to_vec())?;
|
let subdomain = Self::key_to_string(k.to_vec())?;
|
||||||
let website = serde_cbor::from_slice::<Website>(&v)?;
|
let website = rmp_serde::from_slice::<Website>(&v)?;
|
||||||
Ok((subdomain, website))
|
Ok((subdomain, website))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -299,7 +300,7 @@ impl Db {
|
||||||
let key = Self::version_key(subdomain, id);
|
let key = Self::version_key(subdomain, id);
|
||||||
|
|
||||||
let data = self.i.versions.get(&key)?;
|
let data = self.i.versions.get(&key)?;
|
||||||
data.and_then(|data| serde_cbor::from_slice::<Version>(data.as_ref()).ok())
|
data.and_then(|data| rmp_serde::from_slice::<Version>(data.as_ref()).ok())
|
||||||
.ok_or_else(|| DbError::NotExists("version", key))
|
.ok_or_else(|| DbError::NotExists("version", key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,16 +312,16 @@ impl Db {
|
||||||
.i
|
.i
|
||||||
.websites
|
.websites
|
||||||
.update_and_fetch(subdomain, |data| match data {
|
.update_and_fetch(subdomain, |data| match data {
|
||||||
Some(data) => match serde_cbor::from_slice::<Website>(data) {
|
Some(data) => match rmp_serde::from_slice::<Website>(data) {
|
||||||
Ok(mut w) => {
|
Ok(mut w) => {
|
||||||
w.vid_count += 1;
|
w.vid_count += 1;
|
||||||
serde_cbor::to_vec(&w).ok()
|
rmp_serde::to_vec(&w).ok()
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
})?
|
})?
|
||||||
.and_then(|data| serde_cbor::from_slice::<Website>(&data).ok());
|
.and_then(|data| rmp_serde::from_slice::<Website>(&data).ok());
|
||||||
|
|
||||||
let id = match ws {
|
let id = match ws {
|
||||||
Some(ws) => ws.vid_count,
|
Some(ws) => ws.vid_count,
|
||||||
|
@ -328,7 +329,7 @@ impl Db {
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = Self::version_key(subdomain, id);
|
let key = Self::version_key(subdomain, id);
|
||||||
let data = serde_cbor::to_vec(version)?;
|
let data = rmp_serde::to_vec(version)?;
|
||||||
self.i
|
self.i
|
||||||
.versions
|
.versions
|
||||||
.compare_and_swap(&key, None::<&[u8]>, Some(data))?
|
.compare_and_swap(&key, None::<&[u8]>, Some(data))?
|
||||||
|
@ -343,12 +344,12 @@ impl Db {
|
||||||
self.i
|
self.i
|
||||||
.websites
|
.websites
|
||||||
.update_and_fetch(subdomain, |data| match data {
|
.update_and_fetch(subdomain, |data| match data {
|
||||||
Some(data) => match serde_cbor::from_slice::<Website>(data) {
|
Some(data) => match rmp_serde::from_slice::<Website>(data) {
|
||||||
Ok(mut w) => {
|
Ok(mut w) => {
|
||||||
if w.vid_count == version {
|
if w.vid_count == version {
|
||||||
w.vid_count -= 1;
|
w.vid_count -= 1;
|
||||||
}
|
}
|
||||||
serde_cbor::to_vec(&w).ok()
|
rmp_serde::to_vec(&w).ok()
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
|
@ -416,7 +417,7 @@ impl Db {
|
||||||
self.i.versions.scan_prefix(key).map(|r| {
|
self.i.versions.scan_prefix(key).map(|r| {
|
||||||
r.map_err(DbError::from).and_then(|(k, v)| {
|
r.map_err(DbError::from).and_then(|(k, v)| {
|
||||||
let (_, id) = Self::split_version_key(k.to_vec())?;
|
let (_, id) = Self::split_version_key(k.to_vec())?;
|
||||||
let version = serde_cbor::from_slice::<Version>(&v)?;
|
let version = rmp_serde::from_slice::<Version>(&v)?;
|
||||||
Ok((id, version))
|
Ok((id, version))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,8 +12,6 @@ pub struct Website {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Website creation date
|
/// Website creation date
|
||||||
pub created_at: OffsetDateTime,
|
pub created_at: OffsetDateTime,
|
||||||
/// Website update date
|
|
||||||
pub updated_at: OffsetDateTime,
|
|
||||||
/// Latest version ID
|
/// Latest version ID
|
||||||
pub latest_version: Option<u32>,
|
pub latest_version: Option<u32>,
|
||||||
/// Color of the page icon
|
/// Color of the page icon
|
||||||
|
@ -29,17 +27,15 @@ pub struct Website {
|
||||||
/// value + 1 will be the next version ID
|
/// value + 1 will be the next version ID
|
||||||
pub vid_count: u32,
|
pub vid_count: u32,
|
||||||
/// Does the website have an icon?
|
/// Does the website have an icon?
|
||||||
|
#[serde(default)]
|
||||||
pub has_icon: bool,
|
pub has_icon: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Website {
|
impl Default for Website {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let created_at = OffsetDateTime::now_utc();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: Default::default(),
|
name: Default::default(),
|
||||||
created_at,
|
created_at: OffsetDateTime::now_utc(),
|
||||||
updated_at: created_at,
|
|
||||||
latest_version: Default::default(),
|
latest_version: Default::default(),
|
||||||
color: Default::default(),
|
color: Default::default(),
|
||||||
visibility: Default::default(),
|
visibility: Default::default(),
|
||||||
|
|
|
@ -37,7 +37,7 @@ impl ResponseError for ImagesError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMAGE_SIZE: u32 = 48;
|
const IMAGE_SIZE: u32 = 32;
|
||||||
const MAX_IMAGE_SIZE: u32 = 4000;
|
const MAX_IMAGE_SIZE: u32 = 4000;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, ImagesError>;
|
type Result<T> = std::result::Result<T, ImagesError>;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{ops::Deref, path::Path, sync::Arc, time::Duration};
|
use std::{ops::Deref, path::Path, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
assets,
|
assets,
|
||||||
|
@ -10,7 +10,6 @@ use crate::{
|
||||||
storage::Storage,
|
storage::Storage,
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
use clokwerk::{Interval, Scheduler};
|
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use poem::{
|
use poem::{
|
||||||
http::header, listener::TcpListener, middleware, Endpoint, EndpointExt, Route, RouteDomain,
|
http::header, listener::TcpListener, middleware, Endpoint, EndpointExt, Route, RouteDomain,
|
||||||
|
@ -69,7 +68,7 @@ impl Talon {
|
||||||
let storage = Storage::new(storage_dir, db.clone(), cfg.clone());
|
let storage = Storage::new(storage_dir, db.clone(), cfg.clone());
|
||||||
let icons = Icons::new(icons_dir);
|
let icons = Icons::new(icons_dir);
|
||||||
|
|
||||||
let talon = Self {
|
Ok(Self {
|
||||||
i: TalonInner {
|
i: TalonInner {
|
||||||
cfg,
|
cfg,
|
||||||
db,
|
db,
|
||||||
|
@ -78,24 +77,7 @@ impl Talon {
|
||||||
start_time: OffsetDateTime::now_utc(),
|
start_time: OffsetDateTime::now_utc(),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
};
|
})
|
||||||
|
|
||||||
Ok(talon)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scheduler(&self) -> Scheduler {
|
|
||||||
let mut scheduler = Scheduler::new();
|
|
||||||
let talon = self.clone();
|
|
||||||
scheduler
|
|
||||||
.every(Interval::Minutes(self.cfg.server.purge_interval))
|
|
||||||
.run(move || {
|
|
||||||
log::info!("Starting purge");
|
|
||||||
match talon.storage.purge() {
|
|
||||||
Ok((files, freed)) => log::info!("{files} files purged, {freed} bytes freed"),
|
|
||||||
Err(e) => log::error!("purge error: {e}"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
scheduler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endpoint(&self) -> impl Endpoint {
|
pub fn endpoint(&self) -> impl Endpoint {
|
||||||
|
@ -147,9 +129,6 @@ impl Talon {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch(&self) -> Result<()> {
|
pub async fn launch(&self) -> Result<()> {
|
||||||
let scheduler = self.scheduler();
|
|
||||||
let _scheduler_handle = scheduler.watch_thread(Duration::from_secs(1));
|
|
||||||
|
|
||||||
Server::new(TcpListener::bind(&self.i.cfg.server.address))
|
Server::new(TcpListener::bind(&self.i.cfg.server.address))
|
||||||
.run_with_graceful_shutdown(self.endpoint(), Self::shutdown_signal(), None)
|
.run_with_graceful_shutdown(self.endpoint(), Self::shutdown_signal(), None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
4
tests/fixtures/mod.rs
vendored
4
tests/fixtures/mod.rs
vendored
|
@ -75,7 +75,6 @@ fn insert_websites(db: &Db) {
|
||||||
&Website {
|
&Website {
|
||||||
name: "ThetaDev".to_owned(),
|
name: "ThetaDev".to_owned(),
|
||||||
created_at: datetime!(2023-02-18 16:30 +0),
|
created_at: datetime!(2023-02-18 16:30 +0),
|
||||||
updated_at: datetime!(2023-02-18 16:30 +0),
|
|
||||||
latest_version: Some(2),
|
latest_version: Some(2),
|
||||||
color: Some(2068974),
|
color: Some(2068974),
|
||||||
visibility: talon::model::Visibility::Featured,
|
visibility: talon::model::Visibility::Featured,
|
||||||
|
@ -88,7 +87,6 @@ fn insert_websites(db: &Db) {
|
||||||
&Website {
|
&Website {
|
||||||
name: "Spotify-Gender-Ex".to_owned(),
|
name: "Spotify-Gender-Ex".to_owned(),
|
||||||
created_at: datetime!(2023-02-18 16:30 +0),
|
created_at: datetime!(2023-02-18 16:30 +0),
|
||||||
updated_at: datetime!(2023-02-18 16:30 +0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(1947988),
|
color: Some(1947988),
|
||||||
visibility: talon::model::Visibility::Featured,
|
visibility: talon::model::Visibility::Featured,
|
||||||
|
@ -103,7 +101,6 @@ fn insert_websites(db: &Db) {
|
||||||
&Website {
|
&Website {
|
||||||
name: "RustyPipe".to_owned(),
|
name: "RustyPipe".to_owned(),
|
||||||
created_at: datetime!(2023-02-20 18:30 +0),
|
created_at: datetime!(2023-02-20 18:30 +0),
|
||||||
updated_at: datetime!(2023-02-20 18:30 +0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(7943647),
|
color: Some(7943647),
|
||||||
visibility: talon::model::Visibility::Featured,
|
visibility: talon::model::Visibility::Featured,
|
||||||
|
@ -118,7 +115,6 @@ fn insert_websites(db: &Db) {
|
||||||
&Website {
|
&Website {
|
||||||
name: "SvelteKit SPA".to_owned(),
|
name: "SvelteKit SPA".to_owned(),
|
||||||
created_at: datetime!(2023-03-03 22:00 +0),
|
created_at: datetime!(2023-03-03 22:00 +0),
|
||||||
updated_at: datetime!(2023-03-03 22:00 +0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(16727552),
|
color: Some(16727552),
|
||||||
visibility: talon::model::Visibility::Hidden,
|
visibility: talon::model::Visibility::Hidden,
|
||||||
|
|
|
@ -8,7 +8,6 @@ ConfigInner(
|
||||||
root_domain: "example.com",
|
root_domain: "example.com",
|
||||||
internal_subdomain: "talon-i",
|
internal_subdomain: "talon-i",
|
||||||
internal_url: "http://talon-i.example.com",
|
internal_url: "http://talon-i.example.com",
|
||||||
purge_interval: 60,
|
|
||||||
),
|
),
|
||||||
compression: CompressionCfg(
|
compression: CompressionCfg(
|
||||||
gzip_en: true,
|
gzip_en: true,
|
||||||
|
|
|
@ -8,7 +8,6 @@ ConfigInner(
|
||||||
root_domain: "localhost:3000",
|
root_domain: "localhost:3000",
|
||||||
internal_subdomain: "talon",
|
internal_subdomain: "talon",
|
||||||
internal_url: "http://talon.localhost:3000",
|
internal_url: "http://talon.localhost:3000",
|
||||||
purge_interval: 1440,
|
|
||||||
),
|
),
|
||||||
compression: CompressionCfg(
|
compression: CompressionCfg(
|
||||||
gzip_en: true,
|
gzip_en: true,
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
source: tests/tests.rs
|
source: tests/tests.rs
|
||||||
expression: data
|
expression: data
|
||||||
---
|
---
|
||||||
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"updated_at":[2023,51,18,30,0,0,0,0,0],"latest_version":1,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea","vid_count":1,"has_icon":false}}
|
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":1,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea","vid_count":1,"has_icon":false}}
|
||||||
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"updated_at":[2023,62,22,0,0,0,0,0,0],"latest_version":1,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null,"vid_count":1,"has_icon":false}}
|
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":1,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null,"vid_count":1,"has_icon":false}}
|
||||||
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"updated_at":[2023,49,16,30,0,0,0,0,0],"latest_version":1,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github","vid_count":1,"has_icon":false}}
|
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":1,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github","vid_count":1,"has_icon":false}}
|
||||||
{"type":"version","key":"rustypipe:1","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
|
{"type":"version","key":"rustypipe:1","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
|
||||||
{"type":"version","key":"spa:1","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":"200.html","spa":true}}
|
{"type":"version","key":"spa:1","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":"200.html","spa":true}}
|
||||||
{"type":"version","key":"spotify-gender-ex:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
|
{"type":"version","key":"spotify-gender-ex:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
source: tests/tests.rs
|
source: tests/tests.rs
|
||||||
expression: data
|
expression: data
|
||||||
---
|
---
|
||||||
{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"updated_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null,"vid_count":2,"has_icon":false}}
|
{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null,"vid_count":2,"has_icon":false}}
|
||||||
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"updated_at":[2023,51,18,30,0,0,0,0,0],"latest_version":1,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea","vid_count":1,"has_icon":false}}
|
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":1,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea","vid_count":1,"has_icon":false}}
|
||||||
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"updated_at":[2023,62,22,0,0,0,0,0,0],"latest_version":1,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null,"vid_count":1,"has_icon":false}}
|
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":1,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null,"vid_count":1,"has_icon":false}}
|
||||||
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"updated_at":[2023,49,16,30,0,0,0,0,0],"latest_version":1,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github","vid_count":1,"has_icon":false}}
|
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":1,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github","vid_count":1,"has_icon":false}}
|
||||||
{"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"},"fallback":null,"spa":false}}
|
{"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"},"fallback":null,"spa":false}}
|
||||||
{"type":"version","key":"-:2","value":{"created_at":[2023,49,16,52,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1354755231","Version":"v0.1.1"},"fallback":null,"spa":false}}
|
{"type":"version","key":"-:2","value":{"created_at":[2023,49,16,52,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1354755231","Version":"v0.1.1"},"fallback":null,"spa":false}}
|
||||||
{"type":"version","key":"rustypipe:1","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
|
{"type":"version","key":"rustypipe:1","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
|
||||||
|
|
|
@ -6,7 +6,6 @@ expression: "vec![ws1, ws2, ws3]"
|
||||||
Website(
|
Website(
|
||||||
name: "ThetaDev",
|
name: "ThetaDev",
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(2),
|
latest_version: Some(2),
|
||||||
color: Some(2068974),
|
color: Some(2068974),
|
||||||
visibility: featured,
|
visibility: featured,
|
||||||
|
@ -18,7 +17,6 @@ expression: "vec![ws1, ws2, ws3]"
|
||||||
Website(
|
Website(
|
||||||
name: "Spotify-Gender-Ex",
|
name: "Spotify-Gender-Ex",
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(1947988),
|
color: Some(1947988),
|
||||||
visibility: featured,
|
visibility: featured,
|
||||||
|
@ -30,7 +28,6 @@ expression: "vec![ws1, ws2, ws3]"
|
||||||
Website(
|
Website(
|
||||||
name: "RustyPipe",
|
name: "RustyPipe",
|
||||||
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(7943647),
|
color: Some(7943647),
|
||||||
visibility: featured,
|
visibility: featured,
|
||||||
|
|
|
@ -6,7 +6,6 @@ expression: websites
|
||||||
("-", Website(
|
("-", Website(
|
||||||
name: "ThetaDev",
|
name: "ThetaDev",
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(2),
|
latest_version: Some(2),
|
||||||
color: Some(2068974),
|
color: Some(2068974),
|
||||||
visibility: featured,
|
visibility: featured,
|
||||||
|
@ -18,7 +17,6 @@ expression: websites
|
||||||
("rustypipe", Website(
|
("rustypipe", Website(
|
||||||
name: "RustyPipe",
|
name: "RustyPipe",
|
||||||
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(7943647),
|
color: Some(7943647),
|
||||||
visibility: featured,
|
visibility: featured,
|
||||||
|
@ -30,7 +28,6 @@ expression: websites
|
||||||
("spa", Website(
|
("spa", Website(
|
||||||
name: "SvelteKit SPA",
|
name: "SvelteKit SPA",
|
||||||
created_at: (2023, 62, 22, 0, 0, 0, 0, 0, 0),
|
created_at: (2023, 62, 22, 0, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 62, 22, 0, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(16727552),
|
color: Some(16727552),
|
||||||
visibility: hidden,
|
visibility: hidden,
|
||||||
|
@ -42,7 +39,6 @@ expression: websites
|
||||||
("spotify-gender-ex", Website(
|
("spotify-gender-ex", Website(
|
||||||
name: "Spotify-Gender-Ex",
|
name: "Spotify-Gender-Ex",
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
|
||||||
latest_version: Some(1),
|
latest_version: Some(1),
|
||||||
color: Some(1947988),
|
color: Some(1947988),
|
||||||
visibility: featured,
|
visibility: featured,
|
||||||
|
|
|
@ -5,7 +5,6 @@ expression: website
|
||||||
Website(
|
Website(
|
||||||
name: "ThetaDev2",
|
name: "ThetaDev2",
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: "[date]",
|
|
||||||
latest_version: Some(2),
|
latest_version: Some(2),
|
||||||
color: Some(1000),
|
color: Some(1000),
|
||||||
visibility: hidden,
|
visibility: hidden,
|
||||||
|
|
|
@ -3,7 +3,6 @@ address = "127.0.0.1:3000"
|
||||||
root_domain = "example.com"
|
root_domain = "example.com"
|
||||||
internal_subdomain = "talon-i"
|
internal_subdomain = "talon-i"
|
||||||
internal_url = "http://talon-i.example.com"
|
internal_url = "http://talon-i.example.com"
|
||||||
purge_interval = 60
|
|
||||||
|
|
||||||
# Talon compresses files when they are uploaded
|
# Talon compresses files when they are uploaded
|
||||||
# Here you can configure compression algorithms and levels
|
# Here you can configure compression algorithms and levels
|
||||||
|
|
|
@ -8,8 +8,6 @@ use rstest::rstest;
|
||||||
use fixtures::*;
|
use fixtures::*;
|
||||||
use talon::db::{Db, DbError};
|
use talon::db::{Db, DbError};
|
||||||
|
|
||||||
const ICON_SIZE: u32 = 48;
|
|
||||||
|
|
||||||
mod database {
|
mod database {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -85,7 +83,7 @@ mod database {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let website = db.get_website(SUBDOMAIN_1).unwrap();
|
let website = db.get_website(SUBDOMAIN_1).unwrap();
|
||||||
insta::assert_ron_snapshot!(website, {".updated_at" => "[date]"});
|
insta::assert_ron_snapshot!(website);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
@ -615,8 +613,8 @@ mod icons {
|
||||||
assert!(stored_path.is_file());
|
assert!(stored_path.is_file());
|
||||||
|
|
||||||
let stored_img = ImageReader::open(&stored_path).unwrap().decode().unwrap();
|
let stored_img = ImageReader::open(&stored_path).unwrap().decode().unwrap();
|
||||||
assert_eq!(stored_img.height(), ICON_SIZE);
|
assert_eq!(stored_img.height(), 32);
|
||||||
assert_eq!(stored_img.width(), ICON_SIZE);
|
assert_eq!(stored_img.width(), 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -858,11 +856,10 @@ mod api {
|
||||||
resp.assert_status_is_ok();
|
resp.assert_status_is_ok();
|
||||||
|
|
||||||
let ws = tln.db.get_website("test").unwrap();
|
let ws = tln.db.get_website("test").unwrap();
|
||||||
insta::assert_ron_snapshot!(ws, {".created_at" => "[date]", ".updated_at" => "[date]"}, @r###"
|
insta::assert_ron_snapshot!(ws, {".created_at" => "[date]"}, @r###"
|
||||||
Website(
|
Website(
|
||||||
name: "Test",
|
name: "Test",
|
||||||
created_at: "[date]",
|
created_at: "[date]",
|
||||||
updated_at: "[date]",
|
|
||||||
latest_version: None,
|
latest_version: None,
|
||||||
color: Some(1000),
|
color: Some(1000),
|
||||||
visibility: searchable,
|
visibility: searchable,
|
||||||
|
@ -914,11 +911,10 @@ mod api {
|
||||||
resp.assert_status_is_ok();
|
resp.assert_status_is_ok();
|
||||||
|
|
||||||
let ws = tln.db.get_website("-").unwrap();
|
let ws = tln.db.get_website("-").unwrap();
|
||||||
insta::assert_ron_snapshot!(ws, {".updated_at" => "[date]"}, @r###"
|
insta::assert_ron_snapshot!(ws, @r###"
|
||||||
Website(
|
Website(
|
||||||
name: "Test",
|
name: "Test",
|
||||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||||
updated_at: "[date]",
|
|
||||||
latest_version: Some(2),
|
latest_version: Some(2),
|
||||||
color: Some(1000),
|
color: Some(1000),
|
||||||
visibility: searchable,
|
visibility: searchable,
|
||||||
|
@ -971,8 +967,8 @@ mod api {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.decode()
|
.decode()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(got_icon.height(), ICON_SIZE);
|
assert_eq!(got_icon.height(), 32);
|
||||||
assert_eq!(got_icon.width(), ICON_SIZE);
|
assert_eq!(got_icon.width(), 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
let currentWebsite: Website;
|
let currentWebsite: Website;
|
||||||
|
|
||||||
currentWebsiteStore.subscribe((ws) => {
|
currentWebsiteStore.subscribe((ws) => {
|
||||||
|
console.log("current ws changed", ws);
|
||||||
currentWebsite = ws;
|
currentWebsite = ws;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
closeSearch();
|
closeSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchKeyup(e: KeyboardEvent) {
|
function searchKeypress(e: KeyboardEvent) {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
if (!searchText) {
|
if (!searchText) {
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
active={searchOpen || Boolean(searchText).valueOf()}
|
active={searchOpen || Boolean(searchText).valueOf()}
|
||||||
on:click={openSearch}
|
on:click={openSearch}
|
||||||
on:focusout={closeSearch}
|
on:focusout={closeSearch}
|
||||||
on:keyup={searchKeyup}
|
on:keyup={searchKeypress}
|
||||||
bind:input={searchInput}
|
bind:input={searchInput}
|
||||||
bind:text={searchText}
|
bind:text={searchText}
|
||||||
/>
|
/>
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
height: 100%
|
height: 100%
|
||||||
z-index: 999999
|
z-index: 999999
|
||||||
|
|
||||||
padding: 3em 0.4em 0.4em
|
padding: 1em 0.4em
|
||||||
|
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
|
@ -26,9 +26,7 @@
|
||||||
bind:this={inputElm}
|
bind:this={inputElm}
|
||||||
bind:value={text}
|
bind:value={text}
|
||||||
on:focusout
|
on:focusout
|
||||||
on:keypress|stopPropagation
|
on:keyup
|
||||||
on:keydown|stopPropagation
|
|
||||||
on:keyup|stopPropagation
|
|
||||||
use:selectTextOnFocus
|
use:selectTextOnFocus
|
||||||
/>
|
/>
|
||||||
<Icon iconName="search" size={40} scale={0.6} />
|
<Icon iconName="search" size={40} scale={0.6} />
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./Icon.svelte";
|
||||||
import type { Website } from "talon-client";
|
import type { Website } from "talon-client";
|
||||||
import { talonConfig } from "../util/talonData";
|
import { talonConfig } from "../util/talonData";
|
||||||
import { getAbbreviation } from "../util/functions";
|
|
||||||
|
|
||||||
export let website: Website;
|
export let website: Website;
|
||||||
export let size = 40;
|
export let size = 40;
|
||||||
|
@ -16,7 +15,7 @@
|
||||||
? `${talonConfig.internal}/icons/${website.subdomain}`
|
? `${talonConfig.internal}/icons/${website.subdomain}`
|
||||||
: null}
|
: null}
|
||||||
color={website.color}
|
color={website.color}
|
||||||
alt={getAbbreviation(website.name)}
|
alt={website.name.substring(0, 2)}
|
||||||
{size}
|
{size}
|
||||||
{scale}
|
{scale}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
import PageIcon from "./PageIcon.svelte";
|
import PageIcon from "./PageIcon.svelte";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
getSubdomainAndVersion,
|
|
||||||
getWebsiteUrl,
|
|
||||||
getWebsiteVersionUrl,
|
getWebsiteVersionUrl,
|
||||||
isUrl,
|
isUrl,
|
||||||
trimCommit,
|
trimCommit,
|
||||||
|
@ -16,37 +14,28 @@
|
||||||
import Modal from "./Modal.svelte";
|
import Modal from "./Modal.svelte";
|
||||||
import { openModal } from "svelte-modals";
|
import { openModal } from "svelte-modals";
|
||||||
import InstanceInfoModal from "./InstanceInfoModal.svelte";
|
import InstanceInfoModal from "./InstanceInfoModal.svelte";
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
let currentWebsite: Website;
|
let currentWebsite: Website;
|
||||||
currentWebsiteStore.subscribe((ws) => {
|
currentWebsiteStore.subscribe((ws) => {
|
||||||
currentWebsite = ws;
|
currentWebsite = ws;
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentVid: number | null = getSubdomainAndVersion()[1];
|
|
||||||
|
|
||||||
export let isOpen: boolean;
|
export let isOpen: boolean;
|
||||||
|
$: {
|
||||||
onMount(async () => {
|
if (isOpen && currentWebsite) {
|
||||||
const v = await client.websiteSubdomainVersionsGet({
|
client
|
||||||
subdomain: currentWebsite.subdomain,
|
.websiteSubdomainVersionsGet({ subdomain: currentWebsite.subdomain })
|
||||||
});
|
.then((v) => {
|
||||||
|
versions = v;
|
||||||
versions = v;
|
if (v && v.length > 0) {
|
||||||
if (v && v.length > 0) {
|
currentVersion = v[v.length - 1];
|
||||||
latestVersion = v[v.length - 1];
|
}
|
||||||
|
});
|
||||||
if (currentVid !== null) {
|
|
||||||
currentVersion = v.find((v) => v.id == currentVid);
|
|
||||||
} else {
|
|
||||||
currentVersion = latestVersion;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
let versions: Version[] = [];
|
let versions: Version[] = [];
|
||||||
let currentVersion: Version | undefined;
|
let currentVersion: Version = null;
|
||||||
let latestVersion: Version | undefined;
|
|
||||||
|
|
||||||
function getVersionAttr(version: Version): string | null {
|
function getVersionAttr(version: Version): string | null {
|
||||||
return (
|
return (
|
||||||
|
@ -74,12 +63,6 @@
|
||||||
<p class="divider">
|
<p class="divider">
|
||||||
<InlineIcon iconName="question" />
|
<InlineIcon iconName="question" />
|
||||||
Current version #{currentVersion.id}
|
Current version #{currentVersion.id}
|
||||||
|
|
||||||
{#if latestVersion && latestVersion !== currentVersion}
|
|
||||||
<a class="latest-tag" href={getWebsiteUrl(currentWebsite.subdomain)}
|
|
||||||
>Latest: #{latestVersion.id}</a
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</p>
|
</p>
|
||||||
<Tag key="Upload date" value={formatDate(currentVersion.createdAt)} />
|
<Tag key="Upload date" value={formatDate(currentVersion.createdAt)} />
|
||||||
|
|
||||||
|
@ -112,9 +95,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Powered by
|
Powered by
|
||||||
<button class="link" on:click={openInstanceInfo}
|
<button on:click={openInstanceInfo}>Talon {talonConfig.version}</button>
|
||||||
>Talon {talonConfig.version}</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -130,13 +111,4 @@
|
||||||
font-size: 2em
|
font-size: 2em
|
||||||
margin-left: 0.25em
|
margin-left: 0.25em
|
||||||
|
|
||||||
.latest-tag
|
|
||||||
background-color: lime
|
|
||||||
color: values.$color-text-d1
|
|
||||||
margin: 0 1em
|
|
||||||
overflow: hidden
|
|
||||||
white-space: nowrap
|
|
||||||
padding: 0 0.4em
|
|
||||||
border-radius: 1em
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,16 +6,15 @@
|
||||||
font-family: sans-serif
|
font-family: sans-serif
|
||||||
color: values.$color-text
|
color: values.$color-text
|
||||||
|
|
||||||
a, .link
|
a, button
|
||||||
display: inline
|
display: inline
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
background: none
|
background: none
|
||||||
border: none
|
border: none
|
||||||
box-shadow: none
|
box-shadow: none
|
||||||
padding: 0
|
|
||||||
|
|
||||||
.link
|
a
|
||||||
color: var(--talon-color)
|
color: var(--talon-color)
|
||||||
filter: brightness(150%)
|
filter: brightness(150%)
|
||||||
|
|
||||||
|
|
|
@ -13,4 +13,3 @@ $color-base-2: color.scale($color-base, $lightness: 20%)
|
||||||
$color-primary-light: color.scale($color-primary, $lightness: 15%)
|
$color-primary-light: color.scale($color-primary, $lightness: 15%)
|
||||||
$color-primary-dark: color.scale($color-primary, $lightness: -15%)
|
$color-primary-dark: color.scale($color-primary, $lightness: -15%)
|
||||||
$color-text-1: color.scale($color-text, $lightness: -15%)
|
$color-text-1: color.scale($color-text, $lightness: -15%)
|
||||||
$color-text-d1: color.scale($color-base, $lightness: -20%)
|
|
||||||
|
|
|
@ -21,29 +21,6 @@ export function getSubdomain(): string {
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSubdomainAndVersion(): [string, number | null] {
|
|
||||||
const hn = window.location.hostname;
|
|
||||||
const rd_noport = talonConfig.root_domain.split(":", 1)[0];
|
|
||||||
|
|
||||||
if (hn.endsWith("." + rd_noport)) {
|
|
||||||
const subdomainSplit = hn
|
|
||||||
.substring(0, hn.length - rd_noport.length - 1)
|
|
||||||
.split("--", 2);
|
|
||||||
const subdomain = subdomainSplit[0];
|
|
||||||
|
|
||||||
let version =
|
|
||||||
subdomainSplit.length > 1 ? parseInt(subdomainSplit[1].replace(/^v/, "")) : null;
|
|
||||||
if (Number.isNaN(version)) version = null;
|
|
||||||
|
|
||||||
if (subdomain === "x") {
|
|
||||||
return ["-", version];
|
|
||||||
} else {
|
|
||||||
return [subdomain, version];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ["-", null];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWebsiteUrl(subdomain: string): string {
|
export function getWebsiteUrl(subdomain: string): string {
|
||||||
const proto = window.location.protocol;
|
const proto = window.location.protocol;
|
||||||
|
|
||||||
|
@ -81,23 +58,3 @@ export function trimCommit(commit: string | undefined): string | undefined {
|
||||||
export function isMobile(): boolean {
|
export function isMobile(): boolean {
|
||||||
return window.innerWidth < 768;
|
return window.innerWidth < 768;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a 2-letter abbreviation of the website name.
|
|
||||||
*
|
|
||||||
* If the name consists of multiple words
|
|
||||||
* (separated by spaces, underscores or CamelCase), output
|
|
||||||
* the first letters of these words.
|
|
||||||
*
|
|
||||||
* Otherwise output the first letters of the name.
|
|
||||||
*/
|
|
||||||
export function getAbbreviation(name: string): string {
|
|
||||||
const split_sep = name
|
|
||||||
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
||||||
.split(/[ ,.;_-]/)
|
|
||||||
.filter((x) => x.length > 0);
|
|
||||||
if (split_sep.length >= 2) {
|
|
||||||
return split_sep[0].charAt(0) + split_sep[1].charAt(0);
|
|
||||||
}
|
|
||||||
return name.substring(0, 2);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue