Merging remote changes to loonix

This commit is contained in:
shockrah 2025-08-26 07:44:04 -07:00
commit 81509ceaa8
41 changed files with 5357 additions and 2866 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "gitpage/themes/dimension"]
path = gitpage/themes/dimension
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

View File

@ -1,3 +0,0 @@
{
"rust.all_features": true
}

View File

@ -1,9 +0,0 @@
def Settings(**kwargs):
return {
'ls': {
'cargo': {
'features': ['admin'],
'noDefaultFeatures': True
}
}
}

2112
clippable-svc/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"]

View File

@ -1,82 +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()
})
}
/// Returns a List of categories
/// Primarily used on the main page
/// 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)
}

View File

@ -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()
}
}

View File

@ -1,39 +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;
#[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
.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;
}
}

View File

@ -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()
}

View File

@ -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();
}
}

View File

@ -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()
}

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc7637a1b3f7e75322e3dddb3499931dc9cd57804afbf46c7941e6bc5211d03c
size 21076

View File

@ -1,100 +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;
}
.admin-video-li {
color: black;
text-shadow: none;
padding-right: 1em;
}
.admin-video-li:hover {
color: #0a58ca;
text-shadow: none;
}
.align-left {
text-align: left;
}

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0cfdedb04d45802c1c008c9610bda2df8e3482f195bd254c11ebe07205e2bd5d
size 8560

View File

@ -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>

View File

@ -1,84 +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" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" 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 class="vids-meta-list">
<h1>Videos</h1>
<ul class="list-group" id="videos-list"></ul>
</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>

View File

@ -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>

View File

@ -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>

View File

@ -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
service/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

2487
service/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
service/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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 (Stored with Git LFS) Normal file

Binary file not shown.

View 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
View 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
View 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

File diff suppressed because one or more lines are too long

417
service/static/js/main.js Normal file
View 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
View 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);

BIN
service/static/logo.png (Stored with Git LFS) Normal file

Binary file not shown.

1672
service/static/main.css Normal file

File diff suppressed because it is too large Load Diff

BIN
service/static/overlay.png (Stored with Git LFS) Normal file

Binary file not shown.

View 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>

View 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>

View 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" %}

View 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" %}

@ -0,0 +1 @@
Subproject commit 2474081b7846ce0e211c57b86ed84989548a777d