Compare commits
23 Commits
e31edb55ad
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8135f1d530 | |||
| 81509ceaa8 | |||
| 690158ff1c | |||
| 050160c326 | |||
| d709e5fcc5 | |||
| f6d2fcd3f6 | |||
| eb99d97e7b | |||
| 7d813391bd | |||
| 1309ab2324 | |||
| 150d34c38e | |||
| 61b3a5fe80 | |||
| ac200c0c0d | |||
| 9ca54103e5 | |||
| 9671be1ff3 | |||
| 348410853a | |||
| 54af3628e4 | |||
| dc98feef5f | |||
| ba0d75d383 | |||
| 9f78f316c5 | |||
| 988d598a19 | |||
| 1fd47481c8 | |||
| 4416d08994 | |||
| d1e2d80eae |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -3,13 +3,13 @@ msg
|
|||||||
tmp/
|
tmp/
|
||||||
keys/
|
keys/
|
||||||
|
|
||||||
api/target/
|
clippable-svc/target/
|
||||||
api/dev/
|
clippable-svc/dev/
|
||||||
api/vids/
|
clippable-svc/vids/
|
||||||
api/static/js/
|
clippable-svc/static/js/
|
||||||
api/static/dist/
|
clippable-svc/static/dist/
|
||||||
api/thumbs/
|
clippable-svc/thumbs/
|
||||||
api/*.db
|
clippable-svc/*.db
|
||||||
|
|
||||||
build/
|
build/
|
||||||
gitpage/public/
|
gitpage/public/
|
||||||
@@ -29,3 +29,4 @@ ts/dist/
|
|||||||
ts/node_modules/
|
ts/node_modules/
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
api/.vscode/settings.json
|
api/.vscode/settings.json
|
||||||
|
api/.ycm_extra_conf.py
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ stages:
|
|||||||
- build-backend
|
- build-backend
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
|
include:
|
||||||
|
- local: 'ci/cargo.yml'
|
||||||
|
- local: 'ci/docker.yml'
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
image: shockrah/website:latest
|
image: shockrah/website:latest
|
||||||
stage: pages
|
stage: pages
|
||||||
@@ -22,62 +26,67 @@ pages:
|
|||||||
paths:
|
paths:
|
||||||
- public/
|
- public/
|
||||||
|
|
||||||
|
# Webpack bundles everything anyway so both admin/non-admin builds have
|
||||||
|
# the same frontend code.
|
||||||
build-frontend-js:
|
build-frontend-js:
|
||||||
image: codesignal/typescript:v9.6.0
|
image: codesignal/typescript:v9.6.0
|
||||||
stage: build-frontend
|
stage: build-frontend
|
||||||
stage: pages
|
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
- master
|
- master
|
||||||
script:
|
script:
|
||||||
- cd ts/
|
- cd ts/
|
||||||
- npm run setup
|
- npm i
|
||||||
- npm run build
|
- npm run build
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- api/static/
|
- clippable-svc/static/
|
||||||
|
|
||||||
# Literally both of these fail 99% of the time so I'm forgoing them completely
|
build-server-no-admin:
|
||||||
# for now until I find something doesn't suck
|
extends: .cargo-builder
|
||||||
# Builds out the intended zip package
|
|
||||||
build-server-binaries:
|
|
||||||
image: rustlang/rust:nightly
|
|
||||||
stage: build-backend
|
stage: build-backend
|
||||||
stage: pages
|
needs:
|
||||||
only:
|
- build-frontend-js
|
||||||
refs:
|
dependencies:
|
||||||
- master
|
- build-frontend-js
|
||||||
script:
|
script:
|
||||||
- mkdir -p build
|
- cd clippable-svc/
|
||||||
- cp api/templates/ api/static/ build/ -r
|
|
||||||
- cd api/
|
|
||||||
- cargo build --release
|
- cargo build --release
|
||||||
- cd ../
|
- cd ../
|
||||||
- cp api/target/release/clippable-server build/server
|
|
||||||
- cp ./scripts/ build/ -r
|
|
||||||
- cp readme.md build/
|
|
||||||
- sh ./scripts/default-rocket-toml.sh
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- build/
|
|
||||||
|
|
||||||
# Upload built docker image to the local registry
|
build-server-admin-enabled:
|
||||||
deploy-docker-image:
|
extends: .cargo-builder
|
||||||
stage: deploy
|
stage: build-backend
|
||||||
image: docker:stable
|
|
||||||
only:
|
|
||||||
refs:
|
|
||||||
- master
|
|
||||||
needs:
|
needs:
|
||||||
- build-server-binaries
|
- build-frontend-js
|
||||||
dependencies:
|
dependencies:
|
||||||
- build-server-binaries
|
- build-frontend-js
|
||||||
services:
|
script:
|
||||||
- docker:dind
|
- cd clippable-svc/
|
||||||
|
- cargo build --release --features admin
|
||||||
|
- cd ../
|
||||||
|
|
||||||
|
deploy-no-admin-docker:
|
||||||
|
stage: deploy
|
||||||
|
extends: .docker-deploy
|
||||||
|
needs:
|
||||||
|
- build-server-no-admin
|
||||||
|
dependencies:
|
||||||
|
- build-server-no-admin
|
||||||
script:
|
script:
|
||||||
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
|
||||||
- docker build -t registry.gitlab.com/shockrah/clippable .
|
- docker build -t registry.gitlab.com/shockrah/clippable .
|
||||||
- docker push registry.gitlab.com/shockrah/clippable
|
- docker push registry.gitlab.com/shockrah/clippable
|
||||||
|
|
||||||
|
deploy-admin-docker:
|
||||||
|
stage: deploy
|
||||||
|
extends: .docker-deploy
|
||||||
|
needs:
|
||||||
|
- build-server-admin-enabled
|
||||||
|
dependencies:
|
||||||
|
- build-server-admin-enabled
|
||||||
|
script:
|
||||||
|
- docker build -t registry.gitlab.com/shockrah/clippable:admin .
|
||||||
|
- docker push registry.gitlab.com/shockrah/clippable:admin
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
|||||||
[submodule "gitpage/themes/dimension"]
|
[submodule "gitpage/themes/dimension"]
|
||||||
path = gitpage/themes/dimension
|
path = gitpage/themes/dimension
|
||||||
url = https://gitlab.com/dspechnikov/dimension-hugo
|
url = https://gitlab.com/dspechnikov/dimension-hugo
|
||||||
|
[submodule "service/themes/dimension"]
|
||||||
|
path = service/themes/dimension
|
||||||
|
url = https://github.com/your-identity/hugo-theme-dimension.git
|
||||||
|
|||||||
2112
api/Cargo.lock
generated
2112
api/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "clippable-server"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rocket = { version = "0.5.0-rc.1", features = [ "json" ] }
|
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
|
||||||
lazy_static = { version = "1.4.0" }
|
|
||||||
|
|
||||||
# Required only for builds that contain admin features
|
|
||||||
serde_json = { version = "1.0", optional = true }
|
|
||||||
base64 = { version = "0.13.0", optional = true }
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies.rocket_dyn_templates]
|
|
||||||
version = "0.1.0-rc.1"
|
|
||||||
features = ["tera"]
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
admin = ["serde_json", "base64"]
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
use rocket::request::{Outcome, Request, FromRequest};
|
|
||||||
use rocket::async_trait;
|
|
||||||
use rocket::http::Status;
|
|
||||||
|
|
||||||
use crate::db::{self, DB_PATH};
|
|
||||||
|
|
||||||
pub struct ApiKey {
|
|
||||||
// These are used by rocket's driver code/decl macros however cargo
|
|
||||||
// is not able to check those as the code is generated at compile time.
|
|
||||||
// The dead code thing is just to stifle pointless warnings
|
|
||||||
#[allow(dead_code)]
|
|
||||||
uid: String,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
key: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ApiKeyError {
|
|
||||||
Missing,
|
|
||||||
Invalid,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for ApiKey {
|
|
||||||
type Error = ApiKeyError;
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
|
||||||
let key = req.headers().get_one("ADMIN-API-KEY");
|
|
||||||
let uid = req.headers().get_one("ADMIN-API-UID");
|
|
||||||
|
|
||||||
if key.is_none() || uid.is_none() {
|
|
||||||
return Outcome::Failure((Status::Forbidden, ApiKeyError::Missing));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (key, uid) = (key.unwrap(), uid.unwrap());
|
|
||||||
|
|
||||||
println!("Path to use for db file {:?}", DB_PATH.to_string());
|
|
||||||
let db = db::Database::load(DB_PATH.as_str().into()).unwrap();
|
|
||||||
if let Some(stored) = db.get(uid) {
|
|
||||||
if stored == key {
|
|
||||||
return Outcome::Success(ApiKey {
|
|
||||||
key: key.into(),
|
|
||||||
uid: uid.into()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#[cfg(feature = "admin")]
|
|
||||||
|
|
||||||
mod apikey;
|
|
||||||
mod response;
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::Result;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use rocket::data::{Data, ToByteUnit};
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
use rocket::response::Redirect;
|
|
||||||
use response::{bad_request, ok};
|
|
||||||
|
|
||||||
use apikey::ApiKey;
|
|
||||||
use response::ActionResponse;
|
|
||||||
use crate::common::get_clips_dir;
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
pub async fn login_dashboard_redirect() -> Redirect {
|
|
||||||
Redirect::to("/admin/dashboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/dashboard")]
|
|
||||||
pub async fn login_dashboard() -> Template {
|
|
||||||
// This page is basically just a login form
|
|
||||||
// However the rest of the form is present on this page, just hidden
|
|
||||||
let h: HashMap<i32,i32> = HashMap::new(); // does not allocate
|
|
||||||
return Template::render("admin", &h);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/dashboard")]
|
|
||||||
pub async fn dashboard(_key: ApiKey) -> Json<ActionResponse> {
|
|
||||||
// Assuming the api key check doesn't fail we can reply with Ok
|
|
||||||
// at the application level
|
|
||||||
ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[post("/upload-video/<category>/<filename>", data = "<data>")]
|
|
||||||
pub async fn updload_video(_key: ApiKey, category: PathBuf, filename: PathBuf, data: Data<'_>)
|
|
||||||
-> Result<Json<ActionResponse>> {
|
|
||||||
/*
|
|
||||||
* Uploads must have BOTH a valid filename and a category
|
|
||||||
* Without the category the server will simply not find
|
|
||||||
* the correct endpoint to reach and thus will 404
|
|
||||||
*/
|
|
||||||
if util::valid_filename(&filename) == false {
|
|
||||||
return Ok(bad_request(Some("Invalid filename(s)")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let clips = get_clips_dir();
|
|
||||||
fs::create_dir_all(Path::new(&clips).join(&category))?;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We allow up to 200 Megaytes per upload as most short
|
|
||||||
* clips are not going to be very large anyway and this
|
|
||||||
* should be a reasonably high limit for those that want
|
|
||||||
* to upload "large" clips
|
|
||||||
* */
|
|
||||||
let filepath = Path::new(&clips).join(category).join(filename);
|
|
||||||
data.open(250.megabytes()).into_file(filepath).await?;
|
|
||||||
Ok(ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[delete("/remove-video/<category>/<filename>")]
|
|
||||||
pub async fn remove_video(_key: ApiKey, category: PathBuf, filename: PathBuf)
|
|
||||||
-> Result<Json<ActionResponse>> {
|
|
||||||
let clips = get_clips_dir();
|
|
||||||
let path = Path::new(&clips).join(&category).join(&filename);
|
|
||||||
fs::remove_file(path)?;
|
|
||||||
Ok(ok())
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Module handles general responses for the admin feature
|
|
||||||
* Primarily these are responses for Admin related actions
|
|
||||||
* like fetching video's, updating videos and deleting them
|
|
||||||
* as well
|
|
||||||
*/
|
|
||||||
use serde::Serialize;
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
|
|
||||||
const FAIL: &'static str = "fail";
|
|
||||||
const OK: &'static str = "fail";
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct ActionResponse {
|
|
||||||
status: &'static str,
|
|
||||||
code: i32,
|
|
||||||
details: Option<&'static str>
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ok() -> Json<ActionResponse> {
|
|
||||||
Json(ActionResponse {
|
|
||||||
status: OK,
|
|
||||||
code: 200,
|
|
||||||
details: None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bad_request(text: Option<&'static str>) -> Json<ActionResponse> {
|
|
||||||
Json(ActionResponse {
|
|
||||||
status: FAIL,
|
|
||||||
code: 400,
|
|
||||||
details: text
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub fn valid_filename(p: &PathBuf) -> bool {
|
|
||||||
// Checks if a given filename is actually valid or not
|
|
||||||
let mut valid = false;
|
|
||||||
let s = p.file_name().unwrap_or_default().to_string_lossy();
|
|
||||||
for e in [".mp4", ".webm", ".mkv"] {
|
|
||||||
if s.ends_with(e) {
|
|
||||||
valid = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valid
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use std::fs::DirEntry;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::io;
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use crate::common::{get_clips_dir, thumbs_dir};
|
|
||||||
|
|
||||||
/// Describes a category of videos as
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Category {
|
|
||||||
name: String,
|
|
||||||
/// NOTE: this is simply a URI pathname
|
|
||||||
/// EXAMPLE: /thumbnail/<category>/.thumbnail.png
|
|
||||||
thumbnail: String
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a vector of category directories
|
|
||||||
pub fn get_category_dirs(path: &str) -> std::io::Result<Vec<DirEntry>> {
|
|
||||||
let path = std::path::Path::new(path);
|
|
||||||
// Trying to ignore non-directory entries
|
|
||||||
if !path.is_dir() {
|
|
||||||
let e = io::Error::new(io::ErrorKind::NotFound, "Unable to open");
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ret: Vec<DirEntry> = Vec::new();
|
|
||||||
for entry in (std::fs::read_dir(path)?).flatten() {
|
|
||||||
if entry.path().is_dir() {
|
|
||||||
ret.push(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Will return the path to a category's thumb nail assuming it exists.
|
|
||||||
/// If nothing is found then it gives back the URI path to a not-found image
|
|
||||||
pub fn get_category_thumbnail(category: &str) -> std::io::Result<String> {
|
|
||||||
let pathname = format!("{}/{}", thumbs_dir(), &category);
|
|
||||||
let path = Path::new(&pathname);
|
|
||||||
// Assume directory as we're only called from "safe" places
|
|
||||||
let item = path.read_dir()?.find(|file| {
|
|
||||||
if let Ok(file) = file {
|
|
||||||
let name = file.file_name().into_string().unwrap();
|
|
||||||
name == "category-thumbnail.jpg"
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(match item {
|
|
||||||
Some(name) => {
|
|
||||||
let name = name.unwrap().file_name().into_string().unwrap();
|
|
||||||
format!("/thumbnail/{}/{}", category, name)
|
|
||||||
},
|
|
||||||
None => "/static/cantfindshit.jpg".to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WARN: misconfigured servers are just going to get shafted and serve up
|
|
||||||
/// a tonne of 500's
|
|
||||||
#[get("/categories")]
|
|
||||||
pub fn list() -> Json<Vec<Category>> {
|
|
||||||
let dir = get_clips_dir();
|
|
||||||
|
|
||||||
let mut cats: Vec<Category> = Vec::new();
|
|
||||||
if let Ok(dirs) = get_category_dirs(&dir) {
|
|
||||||
// Let's just assume that each item in this directory is a folder
|
|
||||||
// That way we can do this blindly without 9999 allocs
|
|
||||||
for d in dirs {
|
|
||||||
let name = d.file_name().to_string_lossy().to_string();
|
|
||||||
let thumbnail = match get_category_thumbnail(&name) {
|
|
||||||
Ok(s) => s,
|
|
||||||
_ => "/static/cantfindshit.jpg".to_string()
|
|
||||||
};
|
|
||||||
cats.push(Category {name, thumbnail});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Json(cats)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
/// Returns the absolute path to the videos directory
|
|
||||||
pub fn get_clips_dir() -> String {
|
|
||||||
match env::var("CLIPS_DIR") {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(_) => "/media/clips/".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the absolute path to the thumbnails directory
|
|
||||||
pub fn thumbs_dir() -> String {
|
|
||||||
match env::var("THUMBS_DIR") {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(_) => "/media/thumbnails/".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
117
api/src/db.rs
117
api/src/db.rs
@@ -1,117 +0,0 @@
|
|||||||
#[cfg(feature = "admin")]
|
|
||||||
// This module defines a tiny async interface for the "database" that this
|
|
||||||
// project uses for interfacing with the key store
|
|
||||||
|
|
||||||
// WARN: at the moment there are no guarantees as far as data integrity is
|
|
||||||
// concerned. This means there are no real transactions
|
|
||||||
use std::env;
|
|
||||||
use std::fs::OpenOptions;
|
|
||||||
use std::io::{BufWriter, BufReader};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use rocket::serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref DB_PATH: String = {
|
|
||||||
env::var("DB_PATH").unwrap_or("keys.db".into())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Database {
|
|
||||||
// uid's while random are fine to release as public as the key is more
|
|
||||||
// important however ideally neither should be release. Furthermore
|
|
||||||
// the frontend assists in keeping these secret by treating both as
|
|
||||||
// password fields as they are both randomly generated via a script
|
|
||||||
|
|
||||||
// uid -> key
|
|
||||||
users: HashMap<String,String>,
|
|
||||||
#[serde(skip)]
|
|
||||||
path: PathBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
// Opens a handle to a database file
|
|
||||||
// if none is found then one is created with the new path
|
|
||||||
// if there is one then the existing database is used
|
|
||||||
// any thing else is invalid and causes this to return Err
|
|
||||||
pub fn new(path: PathBuf) -> Result<Self, std::io::Error> {
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.open(&path)?;
|
|
||||||
let writer = BufWriter::new(&file);
|
|
||||||
|
|
||||||
// Dummy value to write in place
|
|
||||||
let empty = Database { users: HashMap::new(), path: "".into() };
|
|
||||||
serde_json::to_writer(writer, &empty)?;
|
|
||||||
|
|
||||||
Ok(empty)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(path: PathBuf) -> Result<Self, std::io::Error> {
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.open(&path)?;
|
|
||||||
let reader = BufReader::new(&file);
|
|
||||||
let mut data: Database = serde_json::from_reader(reader)?;
|
|
||||||
|
|
||||||
data.path = path;
|
|
||||||
return Ok(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, uid: &str) -> Option<&String> {
|
|
||||||
return self.users.get(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(&self) -> Result<(), std::io::Error> {
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.open(&self.path)?;
|
|
||||||
let writer = BufWriter::new(file);
|
|
||||||
serde_json::to_writer(writer, &self.path)?;
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, uid: &str) -> Result<(), std::io::Error> {
|
|
||||||
self.users.remove_entry(uid);
|
|
||||||
self.write()
|
|
||||||
}
|
|
||||||
pub fn add(&mut self, key: &str, value: &str) -> Result<(), std::io::Error> {
|
|
||||||
println!("{:?}", self.path);
|
|
||||||
self.users.insert(key.into(), value.into());
|
|
||||||
self.write()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod db_tests {
|
|
||||||
use super::Database;
|
|
||||||
|
|
||||||
const DB: &'static str = "new.db";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_db() {
|
|
||||||
match Database::new(DB.into()) {
|
|
||||||
Ok(db) => println!("Loaded new sample.db: {:?}", db),
|
|
||||||
Err(e) => panic!("Error fetching database: {}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_simple_entries() {
|
|
||||||
match Database::load(DB.into()) {
|
|
||||||
Ok(mut db) => db.add("key", "value").unwrap(),
|
|
||||||
Err(e) => println!("Error adding entries: {}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn remove_simple_entries() {
|
|
||||||
match Database::load(DB.into()) {
|
|
||||||
Ok(mut db) => db.remove("key").unwrap(),
|
|
||||||
Err(e) => println!("Error removing simple entries: {}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//#![feature(decl_macro)]
|
|
||||||
#[macro_use] extern crate rocket;
|
|
||||||
#[macro_use] extern crate lazy_static;
|
|
||||||
use std::env;
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
|
|
||||||
mod page;
|
|
||||||
mod category;
|
|
||||||
mod video;
|
|
||||||
mod thumbnail;
|
|
||||||
mod common;
|
|
||||||
|
|
||||||
#[cfg(feature = "admin")]
|
|
||||||
mod admin;
|
|
||||||
|
|
||||||
#[cfg(feature = "admin")]
|
|
||||||
mod sec;
|
|
||||||
|
|
||||||
#[cfg(feature = "admin")]
|
|
||||||
mod db;
|
|
||||||
|
|
||||||
#[rocket::main]
|
|
||||||
async fn main() {
|
|
||||||
// rid ourselves of random emoji's in logs
|
|
||||||
env::set_var("ROCKET_CLI_COLORS", "false");
|
|
||||||
|
|
||||||
|
|
||||||
if cfg!(feature = "admin") {
|
|
||||||
#[cfg(feature = "admin")]
|
|
||||||
let _ = rocket::build()
|
|
||||||
.mount("/", routes![page::home, page::category, page::video]) // html
|
|
||||||
.mount("/static", routes![page::files]) // files
|
|
||||||
.mount("/api", routes![category::list, video::list]) // json
|
|
||||||
.mount("/thumbnail", routes![thumbnail::get]) // images
|
|
||||||
.mount("/video", routes![video::get_video]) // videos
|
|
||||||
.mount("/admin", routes![
|
|
||||||
admin::login_dashboard_redirect,
|
|
||||||
admin::login_dashboard,
|
|
||||||
admin::dashboard,
|
|
||||||
admin::updload_video,
|
|
||||||
admin::remove_video
|
|
||||||
])
|
|
||||||
.attach(Template::fairing())
|
|
||||||
.launch().await;
|
|
||||||
} else {
|
|
||||||
let _ = rocket::build()
|
|
||||||
.mount("/", routes![page::home, page::category, page::video]) // html
|
|
||||||
.mount("/static", routes![page::files]) // files
|
|
||||||
.mount("/api", routes![category::list, video::list]) // json
|
|
||||||
.mount("/thumbnail", routes![thumbnail::get]) // images
|
|
||||||
.mount("/video", routes![video::get_video]) // videos
|
|
||||||
.attach(Template::fairing())
|
|
||||||
.launch().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
121
api/src/page.rs
121
api/src/page.rs
@@ -1,121 +0,0 @@
|
|||||||
/// This module takes care of returning bare webpage templates
|
|
||||||
/// Most of the work done here is simply to fill the templates
|
|
||||||
/// with the appropriate meta data that is required
|
|
||||||
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
use rocket::fs::NamedFile;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::common::get_clips_dir;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
// TODO: try... literally try to reduce the number of clones that happen here
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref SITENAME: String = {
|
|
||||||
env::var("SITE_NAME").unwrap_or_else(|_| "Clippable".to_string())
|
|
||||||
};
|
|
||||||
static ref SITEDESC: String = {
|
|
||||||
env::var("SITE_DESC").unwrap_or_else(|_| "Short clips".to_string())
|
|
||||||
};
|
|
||||||
static ref SITEURL: String = {
|
|
||||||
env::var("SITE_URL").unwrap_or_else(|_| "#".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default metadata that all web pages must be filled with
|
|
||||||
pub fn default_map() -> HashMap<&'static str, String> {
|
|
||||||
let mut h: HashMap<&'static str, String> = HashMap::new();
|
|
||||||
h.insert("sitetitle", SITENAME.clone());
|
|
||||||
h.insert("sitedesc", SITEDESC.clone());
|
|
||||||
h.insert("siteurl", SITEURL.clone());
|
|
||||||
h
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
pub async fn home() -> Template {
|
|
||||||
let mut h = default_map();
|
|
||||||
h.insert("page", String::from("home"));
|
|
||||||
h.insert("title", SITENAME.clone());
|
|
||||||
|
|
||||||
Template::render("list", &h)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/category/<cat..>")]
|
|
||||||
pub async fn category(cat: PathBuf) -> Template {
|
|
||||||
let mut h = default_map();
|
|
||||||
h.insert("page", String::from("category"));
|
|
||||||
|
|
||||||
// Opengraph meta tags bro
|
|
||||||
let cat = cat.file_name().unwrap().to_string_lossy();
|
|
||||||
h.insert("title", cat.to_string());
|
|
||||||
h.insert("url", format!("/category/{}", cat));
|
|
||||||
h.insert("desc", format!("{} clips", cat));
|
|
||||||
h.insert("ogimg", format!("/thumbnail/{}/category-thumbnail.jpg", cat));
|
|
||||||
|
|
||||||
Template::render("list", &h)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_base_vfile(category: &Path, base: &Path) -> String {
|
|
||||||
// This takes a category and base filename and basically looks for the file
|
|
||||||
// that maps to those parameters
|
|
||||||
// This basically aims to get rid of the extension in the URL so that social
|
|
||||||
// media sites will bother to fetch open graph tags
|
|
||||||
let basename = base.to_string_lossy();
|
|
||||||
let dirname = format!("{}/{}/", get_clips_dir(), category.to_string_lossy());
|
|
||||||
let dir = Path::new(&dirname);
|
|
||||||
let file = dir.read_dir().ok().unwrap().find(|item| {
|
|
||||||
if let Ok(item) = item {
|
|
||||||
let name = item.file_name().into_string().unwrap();
|
|
||||||
name.starts_with(basename.as_ref())
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
match file {
|
|
||||||
Some(ent) => ent.unwrap().file_name().into_string().unwrap(),
|
|
||||||
None => "/static/cantfindshit.jpg".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/clip/<cat>/<file_base>")]
|
|
||||||
pub fn video(cat: PathBuf, file_base: PathBuf) -> Template {
|
|
||||||
let mut h: HashMap<&'static str, &str> = HashMap::new();
|
|
||||||
|
|
||||||
// Look for the file_base + [extension]
|
|
||||||
let file = map_base_vfile(&cat, &file_base);
|
|
||||||
|
|
||||||
let cat = cat.to_string_lossy();
|
|
||||||
|
|
||||||
let mut file_pretty = file.to_string();
|
|
||||||
for c in [".mp4", ".mkv", ".webm"] {
|
|
||||||
file_pretty = file_pretty.replace(c, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
h.insert("title", &file_pretty);
|
|
||||||
|
|
||||||
let url = format!("/clip/{}/{}", &cat, &file);
|
|
||||||
h.insert("url", &url);
|
|
||||||
h.insert("page", "video");
|
|
||||||
h.insert("desc", &SITEDESC);
|
|
||||||
h.insert("category", &cat);
|
|
||||||
h.insert("filename", &file_pretty);
|
|
||||||
|
|
||||||
let thumbnail = format!("/thumbnail/{}/{}", cat, file);
|
|
||||||
h.insert("ogimg", &thumbnail);
|
|
||||||
|
|
||||||
let clip_url = format!("/video/{}/{}", &cat, &file);
|
|
||||||
h.insert("clip_url", &clip_url);
|
|
||||||
|
|
||||||
let clip_thumbnail = format!("/thumbnail/{}/{}.jpg", &cat, &file);
|
|
||||||
h.insert("clip_thumbnail", &clip_thumbnail);
|
|
||||||
|
|
||||||
Template::render("video", &h)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This route handler returns static files that are comprised
|
|
||||||
/// of JS, CSS, the favicon and other static items
|
|
||||||
#[get("/<file..>")]
|
|
||||||
pub async fn files(file: PathBuf) -> Option<NamedFile> {
|
|
||||||
NamedFile::open(Path::new("static/").join(file)).await.ok()
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#[cfg(feature = "admin")]
|
|
||||||
// This module concerns itself with the encrypting/decrypting of passwords
|
|
||||||
// as well as the storage of those items
|
|
||||||
use rocket::tokio::io::AsyncReadExt;
|
|
||||||
use rocket::tokio::fs;
|
|
||||||
|
|
||||||
async fn random_string() -> Result<String, std::io::Error> {
|
|
||||||
// First we read in some bytes from /dev/urandom
|
|
||||||
let mut handle = fs::File::open("/dev/urandom").await?;
|
|
||||||
let mut buffer = [0;32];
|
|
||||||
handle.read(&mut buffer[..]).await?;
|
|
||||||
|
|
||||||
Ok(base64::encode_config(buffer, base64::URL_SAFE_NO_PAD))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod sec_api_tests {
|
|
||||||
use rocket::tokio;
|
|
||||||
use super::random_string;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn generate_string() {
|
|
||||||
println!("{:?}", random_string().await);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
use rocket::fs::NamedFile;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use crate::common::thumbs_dir;
|
|
||||||
|
|
||||||
/// Returns a thumbnail whose formatted name is <video-name>.<ext>.jpg
|
|
||||||
#[get("/<file..>")]
|
|
||||||
pub async fn get(file: PathBuf) -> Option<NamedFile> {
|
|
||||||
let clips_dir = thumbs_dir();
|
|
||||||
|
|
||||||
// Only serve jpg's and png's through this route
|
|
||||||
let file_path = Path::new(&clips_dir).join(file);
|
|
||||||
if file_path.is_file() {
|
|
||||||
return match file_path.extension() {
|
|
||||||
Some(ext) => {
|
|
||||||
match ext == "jpg" {
|
|
||||||
true => NamedFile::open(file_path).await.ok(),
|
|
||||||
false => None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let path = Path::new("static/cantfindshit.jpg");
|
|
||||||
return NamedFile::open(path).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::fs::DirEntry;
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use serde::Serialize;
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use rocket::fs::NamedFile;
|
|
||||||
use crate::common::get_clips_dir;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct VideoPreview {
|
|
||||||
/// Real filename on disk
|
|
||||||
name: String,
|
|
||||||
/// URI path to what the browser can reasonably expect
|
|
||||||
thumbnail: Option<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vid_file_entries(path: &OsStr) -> std::io::Result<Vec<DirEntry>> {
|
|
||||||
let mut dir = get_clips_dir();
|
|
||||||
dir.push('/'); dir.push_str(&path.to_string_lossy());
|
|
||||||
let path = std::path::Path::new(&dir);
|
|
||||||
|
|
||||||
if !path.is_dir() {
|
|
||||||
panic!("<{:?}> is not a valid directory", path);
|
|
||||||
}
|
|
||||||
let mut entries: Vec<DirEntry> = Vec::new();
|
|
||||||
for ent in (path.read_dir()?).flatten() {
|
|
||||||
let name = ent.file_name().into_string().unwrap();
|
|
||||||
if name.ends_with("mkv") || name.ends_with("mp4") || name.ends_with("webm") {
|
|
||||||
entries.push(ent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[get("/category/<cat..>")]
|
|
||||||
pub fn list(cat: PathBuf) -> Option<Json<Vec<VideoPreview>>> {
|
|
||||||
/*
|
|
||||||
* List out the videos to a given category
|
|
||||||
*/
|
|
||||||
// First we have to make sure this given category is even registered with us
|
|
||||||
let file_path = cat.file_name().unwrap_or_else(|| OsStr::new(""));
|
|
||||||
if let Ok(entries) = vid_file_entries(file_path) {
|
|
||||||
let mut previews: Vec<VideoPreview> = Vec::new();
|
|
||||||
// Autismo but at least its bare-able
|
|
||||||
for ent in entries {
|
|
||||||
let name = ent.file_name();
|
|
||||||
let name = name.to_string_lossy();
|
|
||||||
|
|
||||||
let cat = cat.to_string_lossy();
|
|
||||||
let thumbnail = format!("/thumbnail/{}/{}.jpg", cat, name);
|
|
||||||
|
|
||||||
let item = VideoPreview {
|
|
||||||
name: name.to_string(),
|
|
||||||
thumbnail: Some(thumbnail)
|
|
||||||
};
|
|
||||||
previews.push(item);
|
|
||||||
}
|
|
||||||
return Some(Json(previews))
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Endpoint returns the video file itself that is saved on the server
|
|
||||||
#[get("/<cat>/<file>")]
|
|
||||||
pub async fn get_video(cat: PathBuf, file: PathBuf) -> Option<NamedFile> {
|
|
||||||
let clips_dir = get_clips_dir();
|
|
||||||
let path = Path::new(&clips_dir).join(cat).join(file);
|
|
||||||
NamedFile::open(path).await.ok()
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:bc7637a1b3f7e75322e3dddb3499931dc9cd57804afbf46c7941e6bc5211d03c
|
|
||||||
size 21076
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
html, body, div, tag {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
font-size: 100%;
|
|
||||||
vertical-align: baseline;
|
|
||||||
background: #212121;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: whitesmoke;
|
|
||||||
text-shadow:
|
|
||||||
3px 3px 0 #191818,
|
|
||||||
-1px -1px 0 #191818,
|
|
||||||
1px -1px 0 #191818,
|
|
||||||
-1px 1px 0 #191818,
|
|
||||||
1px 1px 0 #191818;
|
|
||||||
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2em;
|
|
||||||
line-height: 1.6em;
|
|
||||||
color: whitesmoke;
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
.video-gallery {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-block {
|
|
||||||
display: block;
|
|
||||||
padding: 1em;
|
|
||||||
width: 400px;
|
|
||||||
line-height: 1em;
|
|
||||||
margin-right: 1em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
border-radius: 1em;
|
|
||||||
|
|
||||||
background: linear-gradient(#191818,#191818,50%,#00ffcc,50%,#00ffcc);
|
|
||||||
background-size: 100% 200%;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
animation: 0.4s ease;
|
|
||||||
}
|
|
||||||
.video-block:hover {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
animation: 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pure-form {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.pure-img {
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
#video-meta {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0cfdedb04d45802c1c008c9610bda2df8e3482f195bd254c11ebe07205e2bd5d
|
|
||||||
size 8560
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Clippable</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="main">
|
|
||||||
<div class="content">
|
|
||||||
<form class="pure-form">
|
|
||||||
<h1>Clippable</h1>
|
|
||||||
<fieldset>
|
|
||||||
<input type="text" placeholder="Search"/>
|
|
||||||
<button class="pure-button pure-button-primary">Search</button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
<div class="video-gallery ">
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
<div class="video-block">
|
|
||||||
<h2>Video title</h2>
|
|
||||||
<p>some random description that's actually really long for some reason</p>
|
|
||||||
<img class="pure-img" src="/img/tmp.png">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/style.css">
|
|
||||||
<title>Clippable Admin Dashboard</title>
|
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
|
|
||||||
|
|
||||||
<script src="/static/dist/bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="content">
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
|
||||||
<a style="bread-crumb-item text-transform: capitalize" class="navbar-brand" href="/">
|
|
||||||
<img src="/static/favicon.png" class="rounded" width="50" height="50">
|
|
||||||
</a>
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="content" id="login-display">
|
|
||||||
<h2>Login to Instance</h2>
|
|
||||||
<form class="form-inline login-form">
|
|
||||||
<div class="input-group mb-2">
|
|
||||||
<input type="password" id="uid" class="form-control mx-sm-3" placeholder="UID code">
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input class="form-check-input" type="checkbox" onclick="toggle_visible('uid')">
|
|
||||||
<label class="form-check-label" for="showuid">Show uid</label>
|
|
||||||
</div> </div>
|
|
||||||
<div class="input-group mb-2">
|
|
||||||
<input type="password" id="apikey" class="form-control mx-sm-3" placeholder="API Key">
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input class="form-check-input" type="checkbox" onclick="toggle_visible('apikey')">
|
|
||||||
<label class="form-check-label" for="showuid">Show key</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<strong id='error' hidden></strong>
|
|
||||||
<button type="button" class="btn btn-info" id="verify-login-btn">Submit</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="content" id="dashboard" hidden>
|
|
||||||
<div class="container">
|
|
||||||
<form>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="video-file">Pick out video file to upload</label>
|
|
||||||
<input type="file" class="form-control" id="video-file">
|
|
||||||
<label for="category">What category should this go in?</label>
|
|
||||||
<input type="text" class="form-control" id="category">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="video-meta" hidden>
|
|
||||||
<p>Video file name: <code id="vmn"></code></p>
|
|
||||||
<p>Video file size: <code id="vms"></code></p>
|
|
||||||
<p>Video file type: <code id="vmt"></code></p>
|
|
||||||
<button type="button" class="btn btn-primary" id="confirm-upload-btn">Upload</button>
|
|
||||||
</div>
|
|
||||||
<div id="upload-response"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
function toggle_visible(id) {
|
|
||||||
var x = document.getElementById(id);
|
|
||||||
if (x.type === "password") {
|
|
||||||
x.type = "text";
|
|
||||||
} else {
|
|
||||||
x.type = "password";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en"><head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/style.css">
|
|
||||||
<title>{{title}}</title>
|
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
|
|
||||||
{% if page == "category" %}
|
|
||||||
<meta property="og:title" content="{{title}}">
|
|
||||||
<meta property="og:site_name" content="Clippable">
|
|
||||||
<meta property="og:url" content="{{url}}">
|
|
||||||
<meta property="og:description" content="{{desc}}">
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
{% if ogimg %}
|
|
||||||
<meta property="og:image" content="{{ogimg}}">
|
|
||||||
{% else %}
|
|
||||||
<meta property="og:image" content="/static/favicon.png">
|
|
||||||
{% endif %}
|
|
||||||
{# Otherwise we defautl to the home tags #}
|
|
||||||
{% else %}
|
|
||||||
<meta property="og:title" content="{{sitetitle}}">
|
|
||||||
<meta property="og:site_name" content="Clippable">
|
|
||||||
<meta property="og:url" content="{{siteurl}}">
|
|
||||||
<meta property="og:description" content="{{sitedesc}}">
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
<meta property="og:image" content="/static/favicon.png">
|
|
||||||
{% endif %}
|
|
||||||
<script src="/static/dist/bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="layout">
|
|
||||||
<div class="content">
|
|
||||||
{% include "navbar" %}
|
|
||||||
<h1>{{title}}</h1>
|
|
||||||
<div class="video-gallery" id="main-container">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
|
||||||
<a style="bread-crumb-item text-transform: capitalize" class="navbar-brand" href="/">
|
|
||||||
<img src="/static/favicon.png" class="rounded" width="50" height="50">
|
|
||||||
</a>
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
{% if page == "category" %}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
|
||||||
<a href="{{url}}">{{title}}</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% elif page == "video" %}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="/category/{{category}}">{{category}}</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
|
||||||
<a href="{{url}}">{{filename}}</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
|
||||||
<a href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en"><head>
|
|
||||||
<meta name="generator" content="Hugo 0.88.1" />
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/style.css">
|
|
||||||
<title>{{title}}</title>
|
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
|
|
||||||
|
|
||||||
{# Required for a better video player #}
|
|
||||||
<link href="//vjs.zencdn.net/7.10.2/video-js.min.css" rel="stylesheet">
|
|
||||||
<script src="//vjs.zencdn.net/7.10.2/video.min.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
{# Opengraph tags #}
|
|
||||||
<meta property="og:title" content="{{title}}">
|
|
||||||
<meta property="og:site_name" content="Clippable">
|
|
||||||
<meta property="og:url" content="{{url}}">
|
|
||||||
<meta property="og:description" content="{{desc}}">
|
|
||||||
<meta property="og:type" content="video.other">
|
|
||||||
<meta property="og:image" content="{{clip_thumbnail}}">
|
|
||||||
{# Image tag requires a default in case of disk memery #}
|
|
||||||
<meta property="og:video" content="{{clip_url}}">
|
|
||||||
|
|
||||||
{% if script %}
|
|
||||||
<script src="/static/dist/bundle.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="layout">
|
|
||||||
<div class="content">
|
|
||||||
{% include "navbar" %}
|
|
||||||
<h1>{{title}}</h1>
|
|
||||||
<video width="1280" height="720" class="video-js" controls autoplay>
|
|
||||||
<source src="{{clip_url}}">
|
|
||||||
wtf get a better browser 1d10t
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
resource "aws_ebs_volume" "app_volume" {
|
|
||||||
availability_zone = var.availability_zone
|
|
||||||
size = 20
|
|
||||||
type = "standard"
|
|
||||||
tags = {
|
|
||||||
Name = "APP Video block storage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_volume_attachment" "ebs_att" {
|
|
||||||
device_name = "/dev/sdf"
|
|
||||||
volume_id = aws_ebs_volume.app_volume.id
|
|
||||||
instance_id = aws_instance.app_instance.id
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# This here module takes care of setting up the ec2 instances that our
|
|
||||||
# containers will bind to later on
|
|
||||||
|
|
||||||
variable "aws_key" {}
|
|
||||||
variable "aws_secret" {}
|
|
||||||
variable "aws_region" {}
|
|
||||||
variable "ami_id" {}
|
|
||||||
variable "instance_type" {}
|
|
||||||
variable "ssh_key_name" {}
|
|
||||||
variable "public_key_path" {}
|
|
||||||
variable "availability_zone" {}
|
|
||||||
|
|
||||||
provider "aws" {
|
|
||||||
access_key = var.aws_key
|
|
||||||
secret_key = var.aws_secret
|
|
||||||
region = var.aws_region
|
|
||||||
max_retries = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_key_pair" "sshkey" {
|
|
||||||
key_name = var.ssh_key_name
|
|
||||||
public_key = file(var.public_key_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_instance" "app_instance" {
|
|
||||||
ami = var.ami_id
|
|
||||||
instance_type = var.instance_type
|
|
||||||
|
|
||||||
key_name = var.ssh_key_name
|
|
||||||
security_groups = [ aws_security_group.app_security_group.id ]
|
|
||||||
subnet_id = aws_subnet.app_public_subnet.id
|
|
||||||
tags = {
|
|
||||||
Name = "Clippable App Instance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
resource "aws_eip" "app_eip" {
|
|
||||||
instance = aws_instance.app_instance.id
|
|
||||||
vpc = true
|
|
||||||
tags = {
|
|
||||||
Name = "Clippable EIP"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
resource "aws_internet_gateway" "app_gateway" {
|
|
||||||
vpc_id = aws_vpc.app_vpc.id
|
|
||||||
tags = {
|
|
||||||
Name = "Clippable app internet gateway"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
resource "aws_route_table" "app_route_table" {
|
|
||||||
vpc_id = aws_vpc.app_vpc.id
|
|
||||||
route {
|
|
||||||
cidr_block = "0.0.0.0/0"
|
|
||||||
gateway_id = aws_internet_gateway.app_gateway.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_route_table_association" "app_subnet_assoc" {
|
|
||||||
subnet_id = aws_subnet.app_public_subnet.id
|
|
||||||
route_table_id = aws_route_table.app_route_table.id
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# This script runs in order to to set things up for so that we don't have to do
|
|
||||||
# much else by ourselves
|
|
||||||
|
|
||||||
# No harm in using sudo even as root its just a little pointless
|
|
||||||
# Doing this with our ami however means we don't have to check if we're root
|
|
||||||
# for privileged operations at provision-time
|
|
||||||
apt="sudo apt"
|
|
||||||
server_name=$1
|
|
||||||
if [ -z "$server_name" ];then
|
|
||||||
echo A servername must be given as an argument
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
$apt update
|
|
||||||
$apt upgrade
|
|
||||||
$apt install -y nginx certbot
|
|
||||||
|
|
||||||
sudo mkdir -p /var/www/clippable
|
|
||||||
# Creating the reverse proxy configuration for nginx
|
|
||||||
# WARN: Also we're assuming that the webserver has the default port
|
|
||||||
# Only this because certbot does the rest
|
|
||||||
cat << EOF > /etc/nginx/sites-available/clippable
|
|
||||||
server {
|
|
||||||
server_name $server_name;
|
|
||||||
location / {
|
|
||||||
proxy_pass http://0.0.0.0:8482;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
resource "aws_security_group" "app_security_group" {
|
|
||||||
name = "App sec group"
|
|
||||||
description = "Allowing SSH and web traffic"
|
|
||||||
vpc_id = aws_vpc.app_vpc.id
|
|
||||||
|
|
||||||
ingress {
|
|
||||||
cidr_blocks = ["0.0.0.0/0"]
|
|
||||||
from_port = 443
|
|
||||||
to_port = 443
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
ingress {
|
|
||||||
cidr_blocks = ["0.0.0.0/0"]
|
|
||||||
from_port = 80
|
|
||||||
to_port = 80
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
ingress {
|
|
||||||
cidr_blocks = ["0.0.0.0/0"]
|
|
||||||
from_port = 22
|
|
||||||
to_port = 22
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
# These are so that we can update the system regularly using apt and sometimes
|
|
||||||
# with tarballs if we're updating something from source
|
|
||||||
egress {
|
|
||||||
cidr_blocks = ["0.0.0.0/0"]
|
|
||||||
from_port = 443
|
|
||||||
to_port = 443
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
egress {
|
|
||||||
cidr_blocks = ["0.0.0.0/0"]
|
|
||||||
from_port = 80
|
|
||||||
to_port = 80
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
resource "aws_subnet" "app_public_subnet" {
|
|
||||||
vpc_id = aws_vpc.app_vpc.id
|
|
||||||
cidr_block = "10.0.0.128/26"
|
|
||||||
availability_zone = var.availability_zone
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
resource "aws_vpc" "app_vpc" {
|
|
||||||
cidr_block = "10.0.0.128/26"
|
|
||||||
enable_dns_support = true
|
|
||||||
enable_dns_hostnames = true
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
Name = "Clippable APP VPC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
Role Name
|
|
||||||
=========
|
|
||||||
|
|
||||||
This role is dedicated to making the setup and administration of a clippable
|
|
||||||
server a little bit easier for those intending on running their own instance.
|
|
||||||
|
|
||||||
There are playbooks for maintaining this service both as a container and as a
|
|
||||||
service running on System D.
|
|
||||||
|
|
||||||
|
|
||||||
Role Variables
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Vars in: `defaults/main.yml`
|
|
||||||
|
|
||||||
* `remote_user`: Default username to use for regular tasks
|
|
||||||
|
|
||||||
Set to `admin` by default.
|
|
||||||
|
|
||||||
* `remote_app_dir`: Directory to install application files into
|
|
||||||
|
|
||||||
Set to `/home/{{remote_user}}/app` by default
|
|
||||||
|
|
||||||
This includes things like the server binary and HTML template files.
|
|
||||||
You only need to worry about this if you're not going to run this
|
|
||||||
in a container.
|
|
||||||
|
|
||||||
* `main_host`
|
|
||||||
|
|
||||||
Set to `main` by default.
|
|
||||||
|
|
||||||
Host that you intend on targeting.
|
|
||||||
|
|
||||||
|
|
||||||
Dependencies
|
|
||||||
------------
|
|
||||||
|
|
||||||
* community.docker
|
|
||||||
|
|
||||||
This is only required if you are planning on using any of the docker playbooks.
|
|
||||||
|
|
||||||
Example Playbook
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
|
||||||
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
GPL V3
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Author: Shockrah
|
|
||||||
Email: dev@shockrah.xyz
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
remote_user: admin
|
|
||||||
remote_app_dir: "/home/{{remote_user}}/app"
|
|
||||||
main_host: main
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
# handlers file for playbooks
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
galaxy_info:
|
|
||||||
author: your name
|
|
||||||
description: your role description
|
|
||||||
company: your company (optional)
|
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
|
||||||
# - BSD-3-Clause (default)
|
|
||||||
# - MIT
|
|
||||||
# - GPL-2.0-or-later
|
|
||||||
# - GPL-3.0-only
|
|
||||||
# - Apache-2.0
|
|
||||||
# - CC-BY-4.0
|
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
|
||||||
|
|
||||||
min_ansible_version: 2.1
|
|
||||||
|
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
|
||||||
# min_ansible_container_version:
|
|
||||||
|
|
||||||
#
|
|
||||||
# Provide a list of supported platforms, and for each platform a list of versions.
|
|
||||||
# If you don't wish to enumerate all versions for a particular platform, use 'all'.
|
|
||||||
# To view available platforms and versions (or releases), visit:
|
|
||||||
# https://galaxy.ansible.com/api/v1/platforms/
|
|
||||||
#
|
|
||||||
# platforms:
|
|
||||||
# - name: Fedora
|
|
||||||
# versions:
|
|
||||||
# - all
|
|
||||||
# - 25
|
|
||||||
# - name: SomePlatform
|
|
||||||
# versions:
|
|
||||||
# - all
|
|
||||||
# - 1.0
|
|
||||||
# - 7
|
|
||||||
# - 99.99
|
|
||||||
|
|
||||||
galaxy_tags: []
|
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
tasks:
|
|
||||||
- name: Install docker dependencies
|
|
||||||
become: yes
|
|
||||||
become_method: sudo
|
|
||||||
apt:
|
|
||||||
name: "{{item}}"
|
|
||||||
update_cache: yes
|
|
||||||
loop:
|
|
||||||
- apt-transport-https
|
|
||||||
- ca-certificates
|
|
||||||
- curl
|
|
||||||
- gnupg
|
|
||||||
- software-properties-common
|
|
||||||
- lsb-release
|
|
||||||
|
|
||||||
- name: Install docker GPG key
|
|
||||||
become: yes
|
|
||||||
become_method: sudo
|
|
||||||
apt_key:
|
|
||||||
url: https://download.docker.com/linux/ubuntu/gpg
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Add Docker Apt Repo
|
|
||||||
become: yes
|
|
||||||
become_method: sudo
|
|
||||||
apt_repository:
|
|
||||||
repo: deb https://download.docker.com/linux/ubuntu impish stable
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Install Docker components
|
|
||||||
become: yes
|
|
||||||
become_method: sudo
|
|
||||||
apt:
|
|
||||||
name: "{{item}}"
|
|
||||||
update_cache: yes
|
|
||||||
loop:
|
|
||||||
- docker-ce
|
|
||||||
- docker-ce-cli
|
|
||||||
- containerd.io
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# This playbook is setup to install docker on debian based systems
|
|
||||||
---
|
|
||||||
- hosts: main
|
|
||||||
tasks:
|
|
||||||
- include_tasks: 'debian.yml'
|
|
||||||
when:
|
|
||||||
ansible_distribution: Debian
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# WHEN TO USE THIS PLAYBOOK:
|
|
||||||
# Use this if you're running clippable under systemd
|
|
||||||
# WHAT THIS PLAYBOOK DOES:
|
|
||||||
# This playbooks basically takes a build/ directory similar to what the Gitlab
|
|
||||||
# pipelines generate and uploads those files to the desired directory
|
|
||||||
---
|
|
||||||
- hosts: {{ main_host }}
|
|
||||||
remote_user: {{ remote_user }}
|
|
||||||
tasks:
|
|
||||||
- name: Build skeleton root directory
|
|
||||||
file:
|
|
||||||
path: '{{remote_app_dir}}'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Build skeleton static directory
|
|
||||||
file:
|
|
||||||
path: '{{remote_app_dir}}/static'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Build skeleton css directory
|
|
||||||
file:
|
|
||||||
path: '{{remote_app_dir}}/static/css'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Build skeleton js directory
|
|
||||||
file:
|
|
||||||
path: '{{remote_app_dir}}/static/js'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Build skeleton templates directory
|
|
||||||
file:
|
|
||||||
path: '{{remote_app_dir}}/templates'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Update Binary installation
|
|
||||||
copy:
|
|
||||||
src: "{{ item.src }}"
|
|
||||||
dest: "{{ item.dest }}"
|
|
||||||
with_items:
|
|
||||||
- { src: ../../build/static/css/style.css, dest: '{{remote_app_dir}}/static/css/'}
|
|
||||||
|
|
||||||
- { src: ../../build/static/js/index.js, dest: '{{remote_app_dir}}/static/js/'}
|
|
||||||
- { src: ../../build/static/js/category.js, dest: '{{remote_app_dir}}/static//js/'}
|
|
||||||
|
|
||||||
- { src: ../../build/static/cantfindshit.jpg, dest: '{{remote_app_dir}}/static/'}
|
|
||||||
- { src: ../../build/static/favicon.png, dest: '{{remote_app_dir}}/static/'}
|
|
||||||
|
|
||||||
- { src: ../../build/templates/list.html.tera, dest: '{{remote_app_dir}}/templates/list.html.tera'}
|
|
||||||
- { src: ../../build/templates/video.html.tera, dest: '{{remote_app_dir}}/templates/video.html.tera'}
|
|
||||||
|
|
||||||
- { src: ../../build/Rocket.toml, dest: '{{remote_app_dir}}/'}
|
|
||||||
- { src: ../../build/server, dest: '{{remote_app_dir}}/'}
|
|
||||||
|
|
||||||
- name: Restart web service
|
|
||||||
become: yes
|
|
||||||
become_method: sudo
|
|
||||||
service:
|
|
||||||
name: app
|
|
||||||
state: restarted
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
localhost
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
- hosts: localhost
|
|
||||||
remote_user: root
|
|
||||||
roles:
|
|
||||||
- playbooks
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
# vars file for playbooks
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# AWS Configuration
|
|
||||||
|
|
||||||
For those that would like to deploy a minimal instance to AWS via terraform
|
|
||||||
this directory will basically have everything you need to get a working instance
|
|
||||||
up and running for very cheap with an EC2 instance.
|
|
||||||
|
|
||||||
There is still the question of preparing the EC2 instance itself however the
|
|
||||||
amount of configuration is very light.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# NOTE: sample inventory either use your own inventory file or just
|
|
||||||
# replace the hostname/ip below
|
|
||||||
[main]
|
|
||||||
1.1.1.1
|
|
||||||
|
|
||||||
16
ci/cargo.yml
Normal file
16
ci/cargo.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.cargo-builder:
|
||||||
|
image: rustlang/rust:nightly
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
before_script:
|
||||||
|
- mkdir -p build
|
||||||
|
- cp clippable-svc/templates/ clippable-svc/static/ build/ -r
|
||||||
|
after_script:
|
||||||
|
- cp clippable-svc/target/release/clippable-server build/server
|
||||||
|
- cp ./scripts/ build/ -r
|
||||||
|
- cp readme.md build/
|
||||||
|
- sh ./scripts/default-rocket-toml.sh
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- build/
|
||||||
9
ci/docker.yml
Normal file
9
ci/docker.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.docker-deploy:
|
||||||
|
image: docker:stable
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
before_script:
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
@@ -10,9 +10,9 @@ cargo build --release
|
|||||||
|
|
||||||
mkdir -p build/
|
mkdir -p build/
|
||||||
|
|
||||||
cp target/release/api build/server
|
cp target/release/clippable-svc build/server
|
||||||
cp api/templates/ build/ -r
|
cp clippable-svc/templates/ build/ -r
|
||||||
cp api/static/ build -r
|
cp clippable-svc/static/ build -r
|
||||||
|
|
||||||
bash ./scripts/default-rocket-toml.sh
|
bash ./scripts/default-rocket-toml.sh
|
||||||
docker build -t registry.gitlab.com/shockrah/clippable .
|
docker build -t registry.gitlab.com/shockrah/clippable .
|
||||||
|
|||||||
1
service/.gitignore
vendored
Normal file
1
service/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
2487
service/Cargo.lock
generated
Normal file
2487
service/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
service/Cargo.toml
Normal file
9
service/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
serde = {version = "1.0", features = ["derive"]}
|
||||||
|
rocket_dyn_templates = {version = "0.2.0", features = ["tera"] }
|
||||||
33
service/src/db.rs
Normal file
33
service/src/db.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use rocket::serde::json;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::DB_FILE;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Row {
|
||||||
|
pub file: String,
|
||||||
|
pub thumbnail: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data = HashMap<String, Row>;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_video(id: &str) -> Option<Row> {
|
||||||
|
// Used to fetch video information from the db for client's to request
|
||||||
|
// video and thumbnail files from us
|
||||||
|
let db_file = fs::read_to_string(DB_FILE)
|
||||||
|
.expect("Unable to read DB_FILE");
|
||||||
|
let db_data: Data = json::from_str(&db_file)
|
||||||
|
.expect("Unable to parse DB_FILE content");
|
||||||
|
|
||||||
|
match db_data.get(id) {
|
||||||
|
Some(video) => Some(Row {
|
||||||
|
file: video.file.clone(),
|
||||||
|
thumbnail: video.thumbnail.clone()
|
||||||
|
}),
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
6
service/src/generic.rs
Normal file
6
service/src/generic.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use rocket_dyn_templates::{context, Template};
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub fn homepage() -> Template {
|
||||||
|
Template::render("index", context!{ })
|
||||||
|
}
|
||||||
24
service/src/main.rs
Normal file
24
service/src/main.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#[macro_use] extern crate rocket;
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use rocket::fs::FileServer;
|
||||||
|
|
||||||
|
mod video;
|
||||||
|
mod db;
|
||||||
|
mod generic;
|
||||||
|
|
||||||
|
const FILES_VIDEO: &'static str = "/opt/clippable/file/video";
|
||||||
|
const DB_FILE: &'static str = "/opt/clippable/db.json";
|
||||||
|
const VIDEO_BASE_URI: &'static str = "/file/video";
|
||||||
|
|
||||||
|
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() {
|
||||||
|
|
||||||
|
let _ = rocket::build()
|
||||||
|
.mount("/", routes![generic::homepage])
|
||||||
|
.mount("/static", FileServer::from("static"))
|
||||||
|
.mount("/c", routes![video::index])
|
||||||
|
.mount(VIDEO_BASE_URI, routes![video::file])
|
||||||
|
.attach(Template::fairing())
|
||||||
|
.launch().await;
|
||||||
|
}
|
||||||
32
service/src/video.rs
Normal file
32
service/src/video.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::{FILES_VIDEO, VIDEO_BASE_URI, db};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use rocket_dyn_templates::{Template, context};
|
||||||
|
|
||||||
|
#[get("/<video_id>")]
|
||||||
|
pub fn index(video_id: &str) -> Template {
|
||||||
|
// Read the db file we have to get the ID
|
||||||
|
// Now we can fetch the row from the DB content
|
||||||
|
match db::get_video(video_id) {
|
||||||
|
Some(video) => {
|
||||||
|
let path = PathBuf::from(&video.file);
|
||||||
|
Template::render("video", context! {
|
||||||
|
title: path.to_str().unwrap(),
|
||||||
|
kind: path.extension().expect("No extension found").to_str().unwrap(),
|
||||||
|
video: format!("{}/{}", VIDEO_BASE_URI, video.file)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
None => Template::render("video", context! {
|
||||||
|
title: "Not found"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[get("/<path>")]
|
||||||
|
pub async fn file(path: PathBuf) -> Option<NamedFile> {
|
||||||
|
let disk = PathBuf::from(FILES_VIDEO).join(path);
|
||||||
|
|
||||||
|
println!("disk path: {disk:?}");
|
||||||
|
NamedFile::open(disk).await.ok()
|
||||||
|
}
|
||||||
BIN
service/static/bg.png
LFS
Normal file
BIN
service/static/bg.png
LFS
Normal file
Binary file not shown.
3
service/static/favicon.png
Normal file
3
service/static/favicon.png
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4582377054b2a53e18c5f212a7f28aaf6915a6765d8f2f2b77b041152e8068ef
|
||||||
|
size 19080
|
||||||
2
service/static/js/breakpoints.min.js
vendored
Normal file
2
service/static/js/breakpoints.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* breakpoints.js v1.0 | @ajlkn | MIT licensed */
|
||||||
|
var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e<t.events.length;e++)n=t.events[e],t.active(n.query)?n.state||(n.state=!0,n.handler()):n.state&&(n.state=!1)}};return e._=t,e.on=function(e,n){t.on(e,n)},e.active=function(e){return t.active(e)},e}();!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():e.breakpoints=t()}(this,function(){return breakpoints});
|
||||||
2
service/static/js/browser.min.js
vendored
Normal file
2
service/static/js/browser.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* browser.js v1.0 | @ajlkn | MIT licensed */
|
||||||
|
var browser=function(){"use strict";var e={name:null,version:null,os:null,osVersion:null,touch:null,mobile:null,_canUse:null,canUse:function(n){e._canUse||(e._canUse=document.createElement("div"));var o=e._canUse.style,r=n.charAt(0).toUpperCase()+n.slice(1);return n in o||"Moz"+r in o||"Webkit"+r in o||"O"+r in o||"ms"+r in o},init:function(){var n,o,r,i,t=navigator.userAgent;for(n="other",o=0,r=[["firefox",/Firefox\/([0-9\.]+)/],["bb",/BlackBerry.+Version\/([0-9\.]+)/],["bb",/BB[0-9]+.+Version\/([0-9\.]+)/],["opera",/OPR\/([0-9\.]+)/],["opera",/Opera\/([0-9\.]+)/],["edge",/Edge\/([0-9\.]+)/],["safari",/Version\/([0-9\.]+).+Safari/],["chrome",/Chrome\/([0-9\.]+)/],["ie",/MSIE ([0-9]+)/],["ie",/Trident\/.+rv:([0-9]+)/]],i=0;i<r.length;i++)if(t.match(r[i][1])){n=r[i][0],o=parseFloat(RegExp.$1);break}for(e.name=n,e.version=o,n="other",o=0,r=[["ios",/([0-9_]+) like Mac OS X/,function(e){return e.replace("_",".").replace("_","")}],["ios",/CPU like Mac OS X/,function(e){return 0}],["wp",/Windows Phone ([0-9\.]+)/,null],["android",/Android ([0-9\.]+)/,null],["mac",/Macintosh.+Mac OS X ([0-9_]+)/,function(e){return e.replace("_",".").replace("_","")}],["windows",/Windows NT ([0-9\.]+)/,null],["bb",/BlackBerry.+Version\/([0-9\.]+)/,null],["bb",/BB[0-9]+.+Version\/([0-9\.]+)/,null],["linux",/Linux/,null],["bsd",/BSD/,null],["unix",/X11/,null]],i=0;i<r.length;i++)if(t.match(r[i][1])){n=r[i][0],o=parseFloat(r[i][2]?r[i][2](RegExp.$1):RegExp.$1);break}e.os=n,e.osVersion=o,e.touch="wp"==e.os?navigator.msMaxTouchPoints>0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser});
|
||||||
2
service/static/js/jquery.min.js
vendored
Normal file
2
service/static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
417
service/static/js/main.js
Normal file
417
service/static/js/main.js
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
Dimension by HTML5 UP
|
||||||
|
html5up.net | @ajlkn
|
||||||
|
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
|
||||||
|
var $window = $(window),
|
||||||
|
$body = $('body'),
|
||||||
|
$wrapper = $('#wrapper'),
|
||||||
|
$header = $('#header'),
|
||||||
|
$footer = $('#footer'),
|
||||||
|
$main = $('#main'),
|
||||||
|
$main_articles = $main.children('article');
|
||||||
|
|
||||||
|
// Breakpoints.
|
||||||
|
breakpoints({
|
||||||
|
xlarge: [ '1281px', '1680px' ],
|
||||||
|
large: [ '981px', '1280px' ],
|
||||||
|
medium: [ '737px', '980px' ],
|
||||||
|
small: [ '481px', '736px' ],
|
||||||
|
xsmall: [ '361px', '480px' ],
|
||||||
|
xxsmall: [ null, '360px' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Play initial animations on page load.
|
||||||
|
$window.on('load', function() {
|
||||||
|
window.setTimeout(function() {
|
||||||
|
$body.removeClass('is-preload');
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix: Flexbox min-height bug on IE.
|
||||||
|
if (browser.name == 'ie') {
|
||||||
|
|
||||||
|
var flexboxFixTimeoutId;
|
||||||
|
|
||||||
|
$window.on('resize.flexbox-fix', function() {
|
||||||
|
|
||||||
|
clearTimeout(flexboxFixTimeoutId);
|
||||||
|
|
||||||
|
flexboxFixTimeoutId = setTimeout(function() {
|
||||||
|
|
||||||
|
if ($wrapper.prop('scrollHeight') > $window.height())
|
||||||
|
$wrapper.css('height', 'auto');
|
||||||
|
else
|
||||||
|
$wrapper.css('height', '100vh');
|
||||||
|
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
}).triggerHandler('resize.flexbox-fix');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nav.
|
||||||
|
var $nav = $header.children('nav'),
|
||||||
|
$nav_li = $nav.find('li');
|
||||||
|
|
||||||
|
// Add "middle" alignment classes if we're dealing with an even number of items.
|
||||||
|
if ($nav_li.length % 2 == 0) {
|
||||||
|
|
||||||
|
$nav.addClass('use-middle');
|
||||||
|
$nav_li.eq( ($nav_li.length / 2) ).addClass('is-middle');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main.
|
||||||
|
var delay = 325,
|
||||||
|
locked = false,
|
||||||
|
nohash = false;
|
||||||
|
|
||||||
|
// Set nohash.
|
||||||
|
if (location.hash == '' || location.hash == '#')
|
||||||
|
nohash = true;
|
||||||
|
|
||||||
|
// Methods.
|
||||||
|
$main._show = function(id, initial) {
|
||||||
|
|
||||||
|
var $article = $main_articles.filter('#' + id);
|
||||||
|
|
||||||
|
// No such article? Bail.
|
||||||
|
if ($article.length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Handle lock.
|
||||||
|
|
||||||
|
// Already locked? Speed through "show" steps w/o delays.
|
||||||
|
if (locked || (typeof initial != 'undefined' && initial === true)) {
|
||||||
|
|
||||||
|
// Mark as switching.
|
||||||
|
$body.addClass('is-switching');
|
||||||
|
|
||||||
|
// Mark as visible.
|
||||||
|
$body.addClass('is-article-visible');
|
||||||
|
|
||||||
|
// Deactivate all articles (just in case one's already active).
|
||||||
|
$main_articles.removeClass('active');
|
||||||
|
|
||||||
|
// Hide header, footer.
|
||||||
|
$header.hide();
|
||||||
|
$footer.hide();
|
||||||
|
|
||||||
|
// Show main, article.
|
||||||
|
$main.show();
|
||||||
|
$article.show();
|
||||||
|
|
||||||
|
// Activate article.
|
||||||
|
$article.addClass('active');
|
||||||
|
|
||||||
|
// Unlock.
|
||||||
|
locked = false;
|
||||||
|
|
||||||
|
// Unmark as switching.
|
||||||
|
setTimeout(function() {
|
||||||
|
$body.removeClass('is-switching');
|
||||||
|
}, (initial ? 1000 : 0));
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock.
|
||||||
|
locked = true;
|
||||||
|
|
||||||
|
// Article already visible? Just swap articles.
|
||||||
|
if ($body.hasClass('is-article-visible')) {
|
||||||
|
|
||||||
|
// Deactivate current article.
|
||||||
|
var $currentArticle = $main_articles.filter('.active');
|
||||||
|
|
||||||
|
$currentArticle.removeClass('active');
|
||||||
|
|
||||||
|
// Show article.
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
// Hide current article.
|
||||||
|
$currentArticle.hide();
|
||||||
|
|
||||||
|
// Show article.
|
||||||
|
$article.show();
|
||||||
|
|
||||||
|
// Activate article.
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
$article.addClass('active');
|
||||||
|
|
||||||
|
// Window stuff.
|
||||||
|
$window
|
||||||
|
.scrollTop(0)
|
||||||
|
.triggerHandler('resize.flexbox-fix');
|
||||||
|
|
||||||
|
// Unlock.
|
||||||
|
setTimeout(function() {
|
||||||
|
locked = false;
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
}, 25);
|
||||||
|
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, handle as normal.
|
||||||
|
else {
|
||||||
|
|
||||||
|
// Mark as visible.
|
||||||
|
$body
|
||||||
|
.addClass('is-article-visible');
|
||||||
|
|
||||||
|
// Show article.
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
// Hide header, footer.
|
||||||
|
$header.hide();
|
||||||
|
$footer.hide();
|
||||||
|
|
||||||
|
// Show main, article.
|
||||||
|
$main.show();
|
||||||
|
$article.show();
|
||||||
|
|
||||||
|
// Activate article.
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
$article.addClass('active');
|
||||||
|
|
||||||
|
// Window stuff.
|
||||||
|
$window
|
||||||
|
.scrollTop(0)
|
||||||
|
.triggerHandler('resize.flexbox-fix');
|
||||||
|
|
||||||
|
// Unlock.
|
||||||
|
setTimeout(function() {
|
||||||
|
locked = false;
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
}, 25);
|
||||||
|
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$main._hide = function(addState) {
|
||||||
|
|
||||||
|
var $article = $main_articles.filter('.active');
|
||||||
|
|
||||||
|
// Article not visible? Bail.
|
||||||
|
if (!$body.hasClass('is-article-visible'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Add state?
|
||||||
|
if (typeof addState != 'undefined'
|
||||||
|
&& addState === true)
|
||||||
|
history.replaceState(null, null, '/');
|
||||||
|
|
||||||
|
// Handle lock.
|
||||||
|
|
||||||
|
// Already locked? Speed through "hide" steps w/o delays.
|
||||||
|
if (locked) {
|
||||||
|
|
||||||
|
// Mark as switching.
|
||||||
|
$body.addClass('is-switching');
|
||||||
|
|
||||||
|
// Deactivate article.
|
||||||
|
$article.removeClass('active');
|
||||||
|
|
||||||
|
// Hide article, main.
|
||||||
|
$article.hide();
|
||||||
|
$main.hide();
|
||||||
|
|
||||||
|
// Show footer, header.
|
||||||
|
$footer.show();
|
||||||
|
$header.show();
|
||||||
|
|
||||||
|
// Unmark as visible.
|
||||||
|
$body.removeClass('is-article-visible');
|
||||||
|
|
||||||
|
// Unlock.
|
||||||
|
locked = false;
|
||||||
|
|
||||||
|
// Unmark as switching.
|
||||||
|
$body.removeClass('is-switching');
|
||||||
|
|
||||||
|
// Window stuff.
|
||||||
|
$window
|
||||||
|
.scrollTop(0)
|
||||||
|
.triggerHandler('resize.flexbox-fix');
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock.
|
||||||
|
locked = true;
|
||||||
|
|
||||||
|
// Deactivate article.
|
||||||
|
$article.removeClass('active');
|
||||||
|
|
||||||
|
// Hide article.
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
// Hide article, main.
|
||||||
|
$article.hide();
|
||||||
|
$main.hide();
|
||||||
|
|
||||||
|
// Show footer, header.
|
||||||
|
$footer.show();
|
||||||
|
$header.show();
|
||||||
|
|
||||||
|
// Unmark as visible.
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
$body.removeClass('is-article-visible');
|
||||||
|
|
||||||
|
// Window stuff.
|
||||||
|
$window
|
||||||
|
.scrollTop(0)
|
||||||
|
.triggerHandler('resize.flexbox-fix');
|
||||||
|
|
||||||
|
// Unlock.
|
||||||
|
setTimeout(function() {
|
||||||
|
locked = false;
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
}, 25);
|
||||||
|
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Articles.
|
||||||
|
$main_articles.each(function() {
|
||||||
|
|
||||||
|
var $this = $(this);
|
||||||
|
|
||||||
|
// Close.
|
||||||
|
$('<div class="close">Close</div>')
|
||||||
|
.appendTo($this)
|
||||||
|
.on('click', function() {
|
||||||
|
if (nohash) {
|
||||||
|
history.go(-1);
|
||||||
|
} else {
|
||||||
|
$main._hide(true);
|
||||||
|
nohash = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent clicks from inside article from bubbling.
|
||||||
|
$this.on('click', function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events.
|
||||||
|
$body.on('click', function(event) {
|
||||||
|
|
||||||
|
// Article visible? Go back. Hide when article is accessed first.
|
||||||
|
if ($body.hasClass('is-article-visible')) {
|
||||||
|
if (nohash) {
|
||||||
|
history.go(-1);
|
||||||
|
} else {
|
||||||
|
$main._hide(true);
|
||||||
|
nohash = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$window.on('keyup', function(event) {
|
||||||
|
|
||||||
|
switch (event.keyCode) {
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
|
||||||
|
// Article visible? Hide.
|
||||||
|
if ($body.hasClass('is-article-visible'))
|
||||||
|
$main._hide(true);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$window.on('hashchange', function(event) {
|
||||||
|
|
||||||
|
// Empty hash?
|
||||||
|
if (location.hash == ''
|
||||||
|
|| location.hash == '#') {
|
||||||
|
|
||||||
|
// Prevent default.
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Hide.
|
||||||
|
$main._hide();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check for a matching article.
|
||||||
|
else if ($main_articles.filter(location.hash).length > 0) {
|
||||||
|
|
||||||
|
// Prevent default.
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Show article.
|
||||||
|
$main._show(location.hash.substr(1));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll restoration.
|
||||||
|
// This prevents the page from scrolling back to the top on a hashchange.
|
||||||
|
if ('scrollRestoration' in history)
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
else {
|
||||||
|
|
||||||
|
var oldScrollPos = 0,
|
||||||
|
scrollPos = 0,
|
||||||
|
$htmlbody = $('html,body');
|
||||||
|
|
||||||
|
$window
|
||||||
|
.on('scroll', function() {
|
||||||
|
|
||||||
|
oldScrollPos = scrollPos;
|
||||||
|
scrollPos = $htmlbody.scrollTop();
|
||||||
|
|
||||||
|
})
|
||||||
|
.on('hashchange', function() {
|
||||||
|
$window.scrollTop(oldScrollPos);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize.
|
||||||
|
|
||||||
|
// Hide main, articles.
|
||||||
|
$main.hide();
|
||||||
|
$main_articles.hide();
|
||||||
|
|
||||||
|
// Initial article.
|
||||||
|
if (location.hash != ''
|
||||||
|
&& location.hash != '#')
|
||||||
|
$window.on('load', function() {
|
||||||
|
$main._show(location.hash.substr(1), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
587
service/static/js/util.js
Normal file
587
service/static/js/util.js
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
(function($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an indented list of links from a nav. Meant for use with panel().
|
||||||
|
* @return {jQuery} jQuery object.
|
||||||
|
*/
|
||||||
|
$.fn.navList = function() {
|
||||||
|
|
||||||
|
var $this = $(this);
|
||||||
|
$a = $this.find('a'),
|
||||||
|
b = [];
|
||||||
|
|
||||||
|
$a.each(function() {
|
||||||
|
|
||||||
|
var $this = $(this),
|
||||||
|
indent = Math.max(0, $this.parents('li').length - 1),
|
||||||
|
href = $this.attr('href'),
|
||||||
|
target = $this.attr('target');
|
||||||
|
|
||||||
|
b.push(
|
||||||
|
'<a ' +
|
||||||
|
'class="link depth-' + indent + '"' +
|
||||||
|
( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
|
||||||
|
( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
|
||||||
|
'>' +
|
||||||
|
'<span class="indent-' + indent + '"></span>' +
|
||||||
|
$this.text() +
|
||||||
|
'</a>'
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return b.join('');
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel-ify an element.
|
||||||
|
* @param {object} userConfig User config.
|
||||||
|
* @return {jQuery} jQuery object.
|
||||||
|
*/
|
||||||
|
$.fn.panel = function(userConfig) {
|
||||||
|
|
||||||
|
// No elements?
|
||||||
|
if (this.length == 0)
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
// Multiple elements?
|
||||||
|
if (this.length > 1) {
|
||||||
|
|
||||||
|
for (var i=0; i < this.length; i++)
|
||||||
|
$(this[i]).panel(userConfig);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vars.
|
||||||
|
var $this = $(this),
|
||||||
|
$body = $('body'),
|
||||||
|
$window = $(window),
|
||||||
|
id = $this.attr('id'),
|
||||||
|
config;
|
||||||
|
|
||||||
|
// Config.
|
||||||
|
config = $.extend({
|
||||||
|
|
||||||
|
// Delay.
|
||||||
|
delay: 0,
|
||||||
|
|
||||||
|
// Hide panel on link click.
|
||||||
|
hideOnClick: false,
|
||||||
|
|
||||||
|
// Hide panel on escape keypress.
|
||||||
|
hideOnEscape: false,
|
||||||
|
|
||||||
|
// Hide panel on swipe.
|
||||||
|
hideOnSwipe: false,
|
||||||
|
|
||||||
|
// Reset scroll position on hide.
|
||||||
|
resetScroll: false,
|
||||||
|
|
||||||
|
// Reset forms on hide.
|
||||||
|
resetForms: false,
|
||||||
|
|
||||||
|
// Side of viewport the panel will appear.
|
||||||
|
side: null,
|
||||||
|
|
||||||
|
// Target element for "class".
|
||||||
|
target: $this,
|
||||||
|
|
||||||
|
// Class to toggle.
|
||||||
|
visibleClass: 'visible'
|
||||||
|
|
||||||
|
}, userConfig);
|
||||||
|
|
||||||
|
// Expand "target" if it's not a jQuery object already.
|
||||||
|
if (typeof config.target != 'jQuery')
|
||||||
|
config.target = $(config.target);
|
||||||
|
|
||||||
|
// Panel.
|
||||||
|
|
||||||
|
// Methods.
|
||||||
|
$this._hide = function(event) {
|
||||||
|
|
||||||
|
// Already hidden? Bail.
|
||||||
|
if (!config.target.hasClass(config.visibleClass))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If an event was provided, cancel it.
|
||||||
|
if (event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide.
|
||||||
|
config.target.removeClass(config.visibleClass);
|
||||||
|
|
||||||
|
// Post-hide stuff.
|
||||||
|
window.setTimeout(function() {
|
||||||
|
|
||||||
|
// Reset scroll position.
|
||||||
|
if (config.resetScroll)
|
||||||
|
$this.scrollTop(0);
|
||||||
|
|
||||||
|
// Reset forms.
|
||||||
|
if (config.resetForms)
|
||||||
|
$this.find('form').each(function() {
|
||||||
|
this.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
}, config.delay);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vendor fixes.
|
||||||
|
$this
|
||||||
|
.css('-ms-overflow-style', '-ms-autohiding-scrollbar')
|
||||||
|
.css('-webkit-overflow-scrolling', 'touch');
|
||||||
|
|
||||||
|
// Hide on click.
|
||||||
|
if (config.hideOnClick) {
|
||||||
|
|
||||||
|
$this.find('a')
|
||||||
|
.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
|
||||||
|
|
||||||
|
$this
|
||||||
|
.on('click', 'a', function(event) {
|
||||||
|
|
||||||
|
var $a = $(this),
|
||||||
|
href = $a.attr('href'),
|
||||||
|
target = $a.attr('target');
|
||||||
|
|
||||||
|
if (!href || href == '#' || href == '' || href == '#' + id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Cancel original event.
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Hide panel.
|
||||||
|
$this._hide();
|
||||||
|
|
||||||
|
// Redirect to href.
|
||||||
|
window.setTimeout(function() {
|
||||||
|
|
||||||
|
if (target == '_blank')
|
||||||
|
window.open(href);
|
||||||
|
else
|
||||||
|
window.location.href = href;
|
||||||
|
|
||||||
|
}, config.delay + 10);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event: Touch stuff.
|
||||||
|
$this.on('touchstart', function(event) {
|
||||||
|
|
||||||
|
$this.touchPosX = event.originalEvent.touches[0].pageX;
|
||||||
|
$this.touchPosY = event.originalEvent.touches[0].pageY;
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
$this.on('touchmove', function(event) {
|
||||||
|
|
||||||
|
if ($this.touchPosX === null
|
||||||
|
|| $this.touchPosY === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
|
||||||
|
diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
|
||||||
|
th = $this.outerHeight(),
|
||||||
|
ts = ($this.get(0).scrollHeight - $this.scrollTop());
|
||||||
|
|
||||||
|
// Hide on swipe?
|
||||||
|
if (config.hideOnSwipe) {
|
||||||
|
|
||||||
|
var result = false,
|
||||||
|
boundary = 20,
|
||||||
|
delta = 50;
|
||||||
|
|
||||||
|
switch (config.side) {
|
||||||
|
|
||||||
|
case 'left':
|
||||||
|
result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'right':
|
||||||
|
result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'top':
|
||||||
|
result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bottom':
|
||||||
|
result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
|
||||||
|
$this.touchPosX = null;
|
||||||
|
$this.touchPosY = null;
|
||||||
|
$this._hide();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent vertical scrolling past the top or bottom.
|
||||||
|
if (($this.scrollTop() < 0 && diffY < 0)
|
||||||
|
|| (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event: Prevent certain events inside the panel from bubbling.
|
||||||
|
$this.on('click touchend touchstart touchmove', function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event: Hide panel if a child anchor tag pointing to its ID is clicked.
|
||||||
|
$this.on('click', 'a[href="#' + id + '"]', function(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
config.target.removeClass(config.visibleClass);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Body.
|
||||||
|
|
||||||
|
// Event: Hide panel on body click/tap.
|
||||||
|
$body.on('click touchend', function(event) {
|
||||||
|
$this._hide(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event: Toggle.
|
||||||
|
$body.on('click', 'a[href="#' + id + '"]', function(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
config.target.toggleClass(config.visibleClass);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window.
|
||||||
|
|
||||||
|
// Event: Hide on ESC.
|
||||||
|
if (config.hideOnEscape)
|
||||||
|
$window.on('keydown', function(event) {
|
||||||
|
|
||||||
|
if (event.keyCode == 27)
|
||||||
|
$this._hide(event);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply "placeholder" attribute polyfill to one or more forms.
|
||||||
|
* @return {jQuery} jQuery object.
|
||||||
|
*/
|
||||||
|
$.fn.placeholder = function() {
|
||||||
|
|
||||||
|
// Browser natively supports placeholders? Bail.
|
||||||
|
if (typeof (document.createElement('input')).placeholder != 'undefined')
|
||||||
|
return $(this);
|
||||||
|
|
||||||
|
// No elements?
|
||||||
|
if (this.length == 0)
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
// Multiple elements?
|
||||||
|
if (this.length > 1) {
|
||||||
|
|
||||||
|
for (var i=0; i < this.length; i++)
|
||||||
|
$(this[i]).placeholder();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vars.
|
||||||
|
var $this = $(this);
|
||||||
|
|
||||||
|
// Text, TextArea.
|
||||||
|
$this.find('input[type=text],textarea')
|
||||||
|
.each(function() {
|
||||||
|
|
||||||
|
var i = $(this);
|
||||||
|
|
||||||
|
if (i.val() == ''
|
||||||
|
|| i.val() == i.attr('placeholder'))
|
||||||
|
i
|
||||||
|
.addClass('polyfill-placeholder')
|
||||||
|
.val(i.attr('placeholder'));
|
||||||
|
|
||||||
|
})
|
||||||
|
.on('blur', function() {
|
||||||
|
|
||||||
|
var i = $(this);
|
||||||
|
|
||||||
|
if (i.attr('name').match(/-polyfill-field$/))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (i.val() == '')
|
||||||
|
i
|
||||||
|
.addClass('polyfill-placeholder')
|
||||||
|
.val(i.attr('placeholder'));
|
||||||
|
|
||||||
|
})
|
||||||
|
.on('focus', function() {
|
||||||
|
|
||||||
|
var i = $(this);
|
||||||
|
|
||||||
|
if (i.attr('name').match(/-polyfill-field$/))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (i.val() == i.attr('placeholder'))
|
||||||
|
i
|
||||||
|
.removeClass('polyfill-placeholder')
|
||||||
|
.val('');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Password.
|
||||||
|
$this.find('input[type=password]')
|
||||||
|
.each(function() {
|
||||||
|
|
||||||
|
var i = $(this);
|
||||||
|
var x = $(
|
||||||
|
$('<div>')
|
||||||
|
.append(i.clone())
|
||||||
|
.remove()
|
||||||
|
.html()
|
||||||
|
.replace(/type="password"/i, 'type="text"')
|
||||||
|
.replace(/type=password/i, 'type=text')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (i.attr('id') != '')
|
||||||
|
x.attr('id', i.attr('id') + '-polyfill-field');
|
||||||
|
|
||||||
|
if (i.attr('name') != '')
|
||||||
|
x.attr('name', i.attr('name') + '-polyfill-field');
|
||||||
|
|
||||||
|
x.addClass('polyfill-placeholder')
|
||||||
|
.val(x.attr('placeholder')).insertAfter(i);
|
||||||
|
|
||||||
|
if (i.val() == '')
|
||||||
|
i.hide();
|
||||||
|
else
|
||||||
|
x.hide();
|
||||||
|
|
||||||
|
i
|
||||||
|
.on('blur', function(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
|
||||||
|
|
||||||
|
if (i.val() == '') {
|
||||||
|
|
||||||
|
i.hide();
|
||||||
|
x.show();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
x
|
||||||
|
.on('focus', function(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
|
||||||
|
|
||||||
|
x.hide();
|
||||||
|
|
||||||
|
i
|
||||||
|
.show()
|
||||||
|
.focus();
|
||||||
|
|
||||||
|
})
|
||||||
|
.on('keypress', function(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
x.val('');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events.
|
||||||
|
$this
|
||||||
|
.on('submit', function() {
|
||||||
|
|
||||||
|
$this.find('input[type=text],input[type=password],textarea')
|
||||||
|
.each(function(event) {
|
||||||
|
|
||||||
|
var i = $(this);
|
||||||
|
|
||||||
|
if (i.attr('name').match(/-polyfill-field$/))
|
||||||
|
i.attr('name', '');
|
||||||
|
|
||||||
|
if (i.val() == i.attr('placeholder')) {
|
||||||
|
|
||||||
|
i.removeClass('polyfill-placeholder');
|
||||||
|
i.val('');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
.on('reset', function(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
$this.find('select')
|
||||||
|
.val($('option:first').val());
|
||||||
|
|
||||||
|
$this.find('input,textarea')
|
||||||
|
.each(function() {
|
||||||
|
|
||||||
|
var i = $(this),
|
||||||
|
x;
|
||||||
|
|
||||||
|
i.removeClass('polyfill-placeholder');
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
|
||||||
|
case 'submit':
|
||||||
|
case 'reset':
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'password':
|
||||||
|
i.val(i.attr('defaultValue'));
|
||||||
|
|
||||||
|
x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
|
||||||
|
|
||||||
|
if (i.val() == '') {
|
||||||
|
i.hide();
|
||||||
|
x.show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i.show();
|
||||||
|
x.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
case 'radio':
|
||||||
|
i.attr('checked', i.attr('defaultValue'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
case 'textarea':
|
||||||
|
i.val(i.attr('defaultValue'));
|
||||||
|
|
||||||
|
if (i.val() == '') {
|
||||||
|
i.addClass('polyfill-placeholder');
|
||||||
|
i.val(i.attr('placeholder'));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
i.val(i.attr('defaultValue'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves elements to/from the first positions of their respective parents.
|
||||||
|
* @param {jQuery} $elements Elements (or selector) to move.
|
||||||
|
* @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
|
||||||
|
*/
|
||||||
|
$.prioritize = function($elements, condition) {
|
||||||
|
|
||||||
|
var key = '__prioritize';
|
||||||
|
|
||||||
|
// Expand $elements if it's not already a jQuery object.
|
||||||
|
if (typeof $elements != 'jQuery')
|
||||||
|
$elements = $($elements);
|
||||||
|
|
||||||
|
// Step through elements.
|
||||||
|
$elements.each(function() {
|
||||||
|
|
||||||
|
var $e = $(this), $p,
|
||||||
|
$parent = $e.parent();
|
||||||
|
|
||||||
|
// No parent? Bail.
|
||||||
|
if ($parent.length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Not moved? Move it.
|
||||||
|
if (!$e.data(key)) {
|
||||||
|
|
||||||
|
// Condition is false? Bail.
|
||||||
|
if (!condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get placeholder (which will serve as our point of reference for when this element needs to move back).
|
||||||
|
$p = $e.prev();
|
||||||
|
|
||||||
|
// Couldn't find anything? Means this element's already at the top, so bail.
|
||||||
|
if ($p.length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Move element to top of parent.
|
||||||
|
$e.prependTo($parent);
|
||||||
|
|
||||||
|
// Mark element as moved.
|
||||||
|
$e.data(key, $p);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moved already?
|
||||||
|
else {
|
||||||
|
|
||||||
|
// Condition is true? Bail.
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
$p = $e.data(key);
|
||||||
|
|
||||||
|
// Move element back to its original location (using our placeholder).
|
||||||
|
$e.insertAfter($p);
|
||||||
|
|
||||||
|
// Unmark element as moved.
|
||||||
|
$e.removeData(key);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
3
service/static/logo.png
Normal file
3
service/static/logo.png
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4582377054b2a53e18c5f212a7f28aaf6915a6765d8f2f2b77b041152e8068ef
|
||||||
|
size 19080
|
||||||
1672
service/static/main.css
Normal file
1672
service/static/main.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
service/static/overlay.png
LFS
Normal file
BIN
service/static/overlay.png
LFS
Normal file
Binary file not shown.
5
service/templates/footer.html.tera
Normal file
5
service/templates/footer.html.tera
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
|
<script src="/static/js/browser.min.js"></script>
|
||||||
|
<script src="/static/js/breakpoints.min.js"></script>
|
||||||
|
<script src="/static/js/util.js"></script>
|
||||||
|
<script src="/static/js/main.js"></script>
|
||||||
13
service/templates/header.html.tera
Normal file
13
service/templates/header.html.tera
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Clippable</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
|
<link rel="stylesheet" href="/static/main.css"/>
|
||||||
|
<link rel="icon" href="/static/favicon.png" type="image/png"
|
||||||
|
<noscript><link rel="stylesheet" href="/static/noscript.css"/></noscript>
|
||||||
|
<style>
|
||||||
|
:root{--site-background : url("/static/bg.png")};
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
31
service/templates/index.html.tera
Normal file
31
service/templates/index.html.tera
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{% include "header" %}
|
||||||
|
<body class="is-preload">
|
||||||
|
<div id="wrapper">
|
||||||
|
<header id="header">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="icon"> <img src="/static/logo.png"> </span>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="inner">
|
||||||
|
<h1>some title</h1>
|
||||||
|
<p> some description </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li> <a href="#about-section">about section</a> </li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div id="main">
|
||||||
|
<article id="about-section">
|
||||||
|
<h2 class="major">about section</h2>
|
||||||
|
<span class="image main"><img src="" alt="" /></span>
|
||||||
|
<p>asdfadf</p>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="bg"></div>
|
||||||
|
</body>
|
||||||
|
{% include "footer" %}
|
||||||
18
service/templates/video.html.tera
Normal file
18
service/templates/video.html.tera
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% include "header" %}
|
||||||
|
<body class="is-preload">
|
||||||
|
<div id="wrapper">
|
||||||
|
<header id="header">
|
||||||
|
<div class="content">
|
||||||
|
<div class="inner">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<video controls>
|
||||||
|
<source src="{{ video }}" type="video/{{ kind }}">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div id="bg"></div>
|
||||||
|
</body>
|
||||||
|
{% include "footer" %}
|
||||||
1
service/themes/dimension
Submodule
1
service/themes/dimension
Submodule
Submodule service/themes/dimension added at 2474081b78
26
ts/admin.ts
26
ts/admin.ts
@@ -1,8 +1,8 @@
|
|||||||
// This module serves as convenience for admin users to upload/remove videos
|
// This module serves as convenience for admin users to upload/remove videos
|
||||||
// from their clippable instance. There are no fancy tricks as this is meant
|
// from their clippable instance. There are no fancy tricks as this is meant
|
||||||
// purely to be a UX thing.
|
// purely to be a UX thing.
|
||||||
//import { fetch_category_videos } from './category'
|
import { fetch_category_videos, VideoMeta } from './category'
|
||||||
//import { fetch_categories } from './index'
|
import { fetch_categories } from './index'
|
||||||
|
|
||||||
|
|
||||||
let UID: null|string = null
|
let UID: null|string = null
|
||||||
@@ -79,7 +79,6 @@ export function populate_meta_form() {
|
|||||||
let file = document.getElementById('video-file') as HTMLInputElement
|
let file = document.getElementById('video-file') as HTMLInputElement
|
||||||
|
|
||||||
// When we remove the file this array becomes 0 so the check is required
|
// When we remove the file this array becomes 0 so the check is required
|
||||||
console.log('files found', file.files.length)
|
|
||||||
if(file.files.length == 0) {
|
if(file.files.length == 0) {
|
||||||
document.getElementById('video-meta').hidden = true
|
document.getElementById('video-meta').hidden = true
|
||||||
} else {
|
} else {
|
||||||
@@ -93,6 +92,24 @@ export function populate_meta_form() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function populate_video_list() {
|
||||||
|
const categories = await fetch_categories()
|
||||||
|
let videos: Array<VideoMeta> = []
|
||||||
|
|
||||||
|
for(const cat of categories) {
|
||||||
|
const vids = await fetch_category_videos(cat.name)
|
||||||
|
for(const v of vids) {
|
||||||
|
videos.push(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list_ref = document.getElementById("videos-list")
|
||||||
|
for(const video of videos) {
|
||||||
|
list_ref.appendChild(video.as_li())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
/*
|
/*
|
||||||
* Setting up hooks required for functionality
|
* Setting up hooks required for functionality
|
||||||
@@ -100,4 +117,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('video-file').onchange = populate_meta_form
|
document.getElementById('video-file').onchange = populate_meta_form
|
||||||
document.getElementById('verify-login-btn').onclick = confirm_auth
|
document.getElementById('verify-login-btn').onclick = confirm_auth
|
||||||
document.getElementById('confirm-upload-btn').onclick = upload_video
|
document.getElementById('confirm-upload-btn').onclick = upload_video
|
||||||
|
populate_video_list()
|
||||||
|
.then(value => console.log('succesful list population: ', value))
|
||||||
|
.catch(reason => console.log('Failure in populate_video_list', reason))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
class VideoMeta {
|
export class VideoMeta {
|
||||||
name: string|null
|
name: string|null
|
||||||
thumbnail: string|null
|
thumbnail: string|null
|
||||||
category: string
|
category: string
|
||||||
basename: string|null
|
basename: string|null
|
||||||
|
href: string
|
||||||
|
|
||||||
constructor(raw: any) {
|
constructor(raw: any) {
|
||||||
this.name = raw['name']
|
this.name = raw['name']
|
||||||
@@ -14,6 +15,8 @@ class VideoMeta {
|
|||||||
this.basename = this.name ?
|
this.basename = this.name ?
|
||||||
this.name.slice(0, this.name.lastIndexOf('.')) :
|
this.name.slice(0, this.name.lastIndexOf('.')) :
|
||||||
null
|
null
|
||||||
|
|
||||||
|
this.href = `/clip/${this.category}/${this.basename}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private clean_link(link: string) : string {
|
private clean_link(link: string) : string {
|
||||||
@@ -28,7 +31,7 @@ class VideoMeta {
|
|||||||
let container = document.createElement('h2')
|
let container = document.createElement('h2')
|
||||||
let link = document.createElement('a')
|
let link = document.createElement('a')
|
||||||
if(this.name) {
|
if(this.name) {
|
||||||
link.href = `/clip/${this.category}/${this.basename}`
|
link.href = this.href
|
||||||
link.text = this.clean_link(this.name)
|
link.text = this.clean_link(this.name)
|
||||||
} else {
|
} else {
|
||||||
link.href = '#'
|
link.href = '#'
|
||||||
@@ -77,10 +80,38 @@ class VideoMeta {
|
|||||||
|
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public as_li() : HTMLElement {
|
||||||
|
const li = document.createElement('li')
|
||||||
|
li.className = 'align-left list-group-item'
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = `/clip/${this.category}/${this.name}`
|
||||||
|
link.target = '_blank'
|
||||||
|
link.textContent = this.name
|
||||||
|
link.className = 'admin-video-li btn'
|
||||||
|
li.appendChild(link)
|
||||||
|
|
||||||
|
const thumbnail_link = document.createElement('a')
|
||||||
|
thumbnail_link.href = `/thumbnail/${this.category}/${this.name}.jpg`
|
||||||
|
link.target = '_blank'
|
||||||
|
thumbnail_link.innerHTML = '<i class="fa-solid fa-image"></i>'
|
||||||
|
thumbnail_link.className = 'admin-video-li btn'
|
||||||
|
li.appendChild(thumbnail_link)
|
||||||
|
|
||||||
|
const delete_btn = document.createElement('button')
|
||||||
|
delete_btn.type = 'button'
|
||||||
|
delete_btn.className = 'btn btn-danger'
|
||||||
|
delete_btn.innerHTML = '<i class="fa-solid fa-trash"></i>'
|
||||||
|
li.appendChild(delete_btn)
|
||||||
|
|
||||||
|
return li
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch_category_videos() : Promise<Array<VideoMeta>> {
|
export async function fetch_category_videos(name?: string) : Promise<Array<VideoMeta>> {
|
||||||
const endpoint = window.location.origin + '/api' + window.location.pathname
|
const category = name ? name : window.location.pathname
|
||||||
|
const endpoint = window.location.origin + `/api/category/${category}`
|
||||||
let videos: Array<VideoMeta> = []
|
let videos: Array<VideoMeta> = []
|
||||||
const response = await fetch(endpoint)
|
const response = await fetch(endpoint)
|
||||||
if(response.headers.get('Content-Type') == 'application/json') {
|
if(response.headers.get('Content-Type') == 'application/json') {
|
||||||
|
|||||||
Reference in New Issue
Block a user