Compare commits

...

7 Commits

Author SHA1 Message Date
4f2fa840d2 remove comment 2021-06-04 00:58:25 +02:00
231f06c2e3 Add: universal task display 2021-06-04 00:50:07 +02:00
737097fbed remove openssl include 2021-06-03 19:42:02 +02:00
e79de5d92e add: chunk conten_provider for file download 2021-06-03 19:08:39 +02:00
3baf88ba0a Add: conan support 2021-06-03 02:54:44 +02:00
Simon Hardt
0ad8735519 Add: Website mounts 2021-06-01 20:14:08 +02:00
Simon Hardt
15404a9c9c del: index.html 2021-06-01 19:49:10 +02:00
15 changed files with 425 additions and 16087 deletions

3
.gitignore vendored
View File

@@ -20,3 +20,6 @@ CMakeSettings.json
Modules/Website/elm-stuff/ Modules/Website/elm-stuff/
Modules/Website/.vscode/settings.json Modules/Website/.vscode/settings.json
build/
.vscode/settings.json

View File

@@ -1,5 +1,8 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
option(USE_VCPKG "Use Vcpkg" OFF)
option(USE_CONAN "use conan" ON)
if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VCPKG_ROOT}) if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VCPKG_ROOT})
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake") set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
endif() endif()
@@ -7,14 +10,21 @@ message("using vcpkg toolchain: ${CMAKE_TOOLCHAIN_FILE}" )
project(localTube) project(localTube)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
if(NOT USE_CONAN)
find_package(fmt CONFIG REQUIRED)
find_package(SOCI CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(Boost COMPONENTS system filesystem REQUIRED)
find_package(unofficial-sqlite3 CONFIG REQUIRED)
elseif(USE_CONAN)
find_package(fmt CONFIG REQUIRED) include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
find_package(soci CONFIG REQUIRED) conan_basic_setup()
find_package(spdlog CONFIG REQUIRED) endif()
find_package(nlohmann_json CONFIG REQUIRED)
find_package(Boost COMPONENTS system filesystem REQUIRED)
add_subdirectory(Modules) add_subdirectory(Modules)

View File

@@ -5,19 +5,29 @@ set(src_files
src/api.cpp src/api.cpp
src/worker.hpp src/worker.hpp
src/worker.cpp src/worker.cpp
src/tables.hpp) src/tables.hpp
src/file_status.hpp
src/file_status.cpp)
add_executable(. ${src_files}) add_executable(localTubeServer ${src_files})
target_link_libraries(. if(USE_CONAN)
fmt::fmt set(LIBS ${CONAN_LIBS})
spdlog::spdlog else()
SOCI::soci_core set(LIBS
SOCI::soci_empty fmt::fmt
SOCI::soci_sqlite3 spdlog::spdlog
nlohmann_json::nlohmann_json SOCI::soci_core
Boost::boost SOCI::soci_empty
${Boost_FILESYSTEM_LIBRARY} SOCI::soci_sqlite3
${Boost_SYSTEM_LIBRARY}) nlohmann_json::nlohmann_json
Boost::boost
${Boost_FILESYSTEM_LIBRARY}
${Boost_SYSTEM_LIBRARY})
endif()
target_link_libraries(localTubeServer ${LIBS})

View File

@@ -7,6 +7,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <functional> #include <functional>
#include <exception>
#include <soci/boost-optional.h> #include <soci/boost-optional.h>
@@ -14,9 +15,10 @@
#include <fmt/chrono.h> #include <fmt/chrono.h>
#include <fmt/ostream.h> #include <fmt/ostream.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <openssl/md5.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "file_status.hpp"
using json = nlohmann::json; using json = nlohmann::json;
Api::Api(soci::session& sql, httplib::Server& server) : sql(sql) Api::Api(soci::session& sql, httplib::Server& server) : sql(sql)
@@ -117,53 +119,10 @@ void Api::files(httplib::Request const& rq, httplib::Response& rs)
{ {
try try
{ {
auto data = TaskInfo::getAll(sql);
json res; json res;
res["files"] = data;
// Query incomplete Tasks
soci::rowset<Task> data = (sql.prepare << "SELECT * "
"FROM Tasks "
"WHERE status!=2 "
"ORDER BY id DESC ");
for (auto const& el : data)
{
json current;
current["hash"] = el.hash;
current["status"] = magic_enum::enum_name(el.status);
current["status_id"] = static_cast<int>(el.status);
current["source"] = el.url;
current["name"] = fmt::format("File {}", el.id);
current["audio_only"] = el.audio_only;
current["format"] = el.format.value_or("Source");
res["queue"].push_back(current);
}
// Downloaded
soci::rowset<soci::row> files =
(sql.prepare
<< R"(SELECT Tasks.id, Files.timestamp, Files.hash, audio_only, filename, Files.format
FROM Files,
Tasks
WHERE Tasks.hash == Files.hash
ORDER BY Files.timestamp DESC )");
for (auto const& el : files)
{
json current;
std::time_t time = el.get<int>("timestamp");
current["timestamp"] = fmt::format("{:%Y-%m-%d %H:%M:%S}", *std::localtime(&time));
current["hash"] = el.get<std::string>("hash");
current["audio_only"] = el.get<int>("audio_only");
current["filename"] = el.get<std::string>("filename");
current["format"] = el.get<std::string>("format");
res["files"].push_back(current);
}
rs.set_content(res.dump(), "application/json"); rs.set_content(res.dump(), "application/json");
} }
@@ -192,15 +151,29 @@ void Api::file(const httplib::Request& rq, httplib::Response& rs)
sql << fmt::format("SELECT * FROM Files WHERE hash='{}';", file), soci::into(dw); sql << fmt::format("SELECT * FROM Files WHERE hash='{}';", file), soci::into(dw);
std::ifstream fs(dw.local_path, std::ios_base::binary); auto fs =
fs.seekg(0, std::ios_base::end); std::make_shared<std::ifstream>(dw.local_path, std::ios_base::binary | std::ios::in);
auto size = fs.tellg(); fs->seekg(0, std::ios_base::end);
fs.seekg(0); auto size = fs->tellg();
rs.body.resize(static_cast<size_t>(size)); fs->seekg(0);
fs.read(&rs.body[0], static_cast<std::streamsize>(size));
rs.set_header("Content-Disposition", fmt::format("attachment; filename={};", dw.filename)); rs.set_header("Content-Disposition", fmt::format("attachment; filename={};", dw.filename));
rs.set_header("content-type", fmt::format("application/{}", dw.format));
rs.set_content_provider(
static_cast<size_t>(size),
fmt::format("application/{}", dw.format).c_str(),
[file = fs](size_t offset, size_t length, httplib::DataSink& sink) mutable {
size_t size = std::min(length, (size_t)1024 * 500);
std::vector<char> data;
data.resize(size);
file->seekg(offset);
file->read(data.data(), data.size());
sink.write(data.data(), data.size());
return true;
});
} }
catch (std::exception const& e) catch (std::exception const& e)
{ {
@@ -221,54 +194,26 @@ void Api::status(httplib::Request const& rq, httplib::Response& rs)
std::string hash = rq.matches[1]; std::string hash = rq.matches[1];
boost::optional<Task> data;
sql << "SELECT * FROM Tasks WHERE hash=:hash", soci::use(hash, "hash"), soci::into(data);
/*soci::rowset<Task> data = (sql.prepare << "SELECT * "
"FROM Tasks "
"WHERE hash=:hash ",
soci::use(hash, "hash"));*/
json response; json response;
try
auto& status = response["status"];
if (data)
{ {
auto const& el = data.value(); auto data = TaskInfo::getId(sql, hash);
status["name"] = fmt::format("Task {}", el.hash); if (data)
status["source"] = el.url;
status["status"] = magic_enum::enum_name(el.status);
status["status_id"] = static_cast<int>(el.status);
status["timestamp"] = el.timestamp;
status["task_file_type"] =
fmt::format("{}: {}", el.audio_only ? "Audio" : "Video", el.format.value_or("Auto"));
status["id"] = el.hash;
if (el.status == FileStatus::COMPLETE)
{ {
boost::optional<File> file_data; response["status"] = data.value();
sql << "SELECT * FROM Files WHERE hash=:hash", soci::use(hash, "hash"),
soci::into(file_data);
if (file_data) } else
{ {
auto const& file = file_data.value(); rs.status = 404;
response["error"] = fmt::format("File '{}' not found!", hash);
auto& file_status = status["file"];
file_status["timestamp"] = file.timestamp;
file_status["name"] = file.filename;
file_status["format"] =
fmt::format("{}: {}", el.audio_only ? "Audio" : "Video", file.format);
}
} }
}
} else catch (std::exception const& e)
{ {
spdlog::error("[Api Status] Excepion: {}", e.what());
rs.status = 404; rs.status = 404;
response["error"] = fmt::format("File '{}' not found!", hash); response["error"] = fmt::format("Exception: {}", e.what());
} }
rs.set_content(response.dump(), "application/json"); rs.set_content(response.dump(), "application/json");
} }

View File

@@ -0,0 +1,76 @@
#include "file_status.hpp"
#include <vector>
#include <soci/boost-optional.h>
#include "tables.hpp"
std::optional<TaskInfo> TaskInfo::getId(soci::session& sql, std::string_view hash)
{
auto hash_str = std::string { hash };
soci::rowset<soci::row> row_data = (sql.prepare << R"(--
SELECT Tasks.hash,
Tasks.status,
Tasks.timestamp AS TaskTimestamp,
Tasks.url,
Files.timestamp AS FileTimestamp,
Files.filename,
Files.format
FROM Tasks LEFT OUTER JOIN Files ON Files.id = Tasks.id
WHERE Tasks.hash = :file_hash ;)",
soci::use(hash_str, "file_hash"));
for (auto const& el : row_data)
{
return fromRow(el);
}
return {};
}
std::vector<TaskInfo> TaskInfo::getAll(soci::session& sql)
{
soci::rowset<soci::row> row_data(sql.prepare << R"(--
SELECT Tasks.hash,
Tasks.status,
Tasks.timestamp AS TaskTimestamp,
Tasks.url,
Files.timestamp AS FileTimestamp,
Files.filename,
Files.format
FROM Tasks LEFT JOIN Files ON Files.id = Tasks.id
ORDER BY TaskTimestamp DESC;")");
std::vector<TaskInfo> data;
for (auto const& el : row_data)
{
data.push_back(fromRow(el));
}
return data;
}
TaskInfo TaskInfo::fromRow(soci::row const& row)
{
TaskInfo status;
auto file_status = row.get<FileStatus>("status");
status.hash = row.get<std::string>("hash");
status.name = fmt::format("Task {}", status.hash);
status.status = magic_enum::enum_name(file_status);
status.timestamp = row.get<int>("TaskTimestamp");
status.url = row.get<std::string>("url");
if (file_status == FileStatus::COMPLETE)
{
status.file = { .timestamp = row.get<int>("FileTimestamp"),
.filename = row.get<std::string>("filename"),
.format = row.get<std::string>("format") };
}
return status;
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
#include <soci/session.h>
using json = nlohmann::json;
struct FileInfo
{
int64_t timestamp;
std::string filename;
std::string format;
};
struct TaskInfo
{
std::string name;
std::string hash;
std::string status;
std::string url;
int64_t timestamp;
std::optional<FileInfo> file;
static std::optional<TaskInfo> getId(soci::session& sql, std::string_view hash);
static std::vector<TaskInfo> getAll(soci::session& sql);
static TaskInfo fromRow(soci::row const& row);
};
inline void to_json(json& j, const TaskInfo& p)
{
j = json { { "name", p.name },
{ "source", p.url },
{ "status", p.status },
{ "timestamp", p.timestamp },
{ "hash", p.hash } };
if(p.file)
{
j["file"]["timestamp"] = p.file.value().timestamp;
j["file"]["filename"] = p.file.value().filename;
j["file"]["format"] = p.file.value().format;
}
}

View File

@@ -69,10 +69,13 @@ int main()
httplib::Server srv; httplib::Server srv;
srv.set_mount_point("/", "www/"); srv.set_mount_point("/", "www/");
srv.set_mount_point("/test", "www/"); srv.set_mount_point("/files", "www/");
srv.set_mount_point("/status", "www/");
Api api(sql, srv); Api api(sql, srv);
spdlog::info("Listing on 0.0.0.0 Port 80"); spdlog::info("Listing on 0.0.0.0 Port 80");
srv.listen("0.0.0.0", 80);
srv.listen("0.0.0.0", 8888);
} }

View File

@@ -124,12 +124,21 @@ bool Worker::download(Task& task)
args += task.format.value(); args += task.format.value();
} }
args += "--restrict-filenames";
args += task.url; args += task.url;
/* ** Download ** */ /* ** Download ** */
spdlog::debug("youtube-dl {}", fmt::join(args, " ")); spdlog::debug("youtube-dl {}", fmt::join(args, " "));
bp::child c("youtube-dl.exe", bp::args(args), bp::std_out > out, bp::std_err > err); #ifdef __linux__
constexpr std::string_view youtube_dl = "youtube-dl";
#elif _WIN32
constexpr std::string_view youtube_dl = "youtube-dl.exe";
#else
#endif
bp::child c(std::string{youtube_dl}, bp::args(args), bp::std_out > out, bp::std_err > err);
std::string destination_file; std::string destination_file;

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,32 @@
module Files exposing (..) module Files exposing (..)
import String
import Debug exposing (toString)
import Element exposing (..) import Element exposing (..)
import Element.Border as Border import Element.Border as Border
import Element.Font as Font import Element.Font as Font
import Element.Region as Region import Element.Region as Region
import Globals exposing (apiFiles) import Globals exposing (apiFiles)
import Http import Http
import Json.Decode exposing (Decoder, int, list, string, succeed) import Json.Decode exposing (Decoder, list, succeed)
import Json.Decode.Pipeline exposing (required) import Json.Decode.Pipeline exposing (required)
import TaskStatus
type alias File =
{ name : String
, file_name : String
, source : String
, id : Int
, status : String
}
type alias FilesResponse =
{ queue : List File }
-- Model -- -- Model --
type alias Model = type Model
{ files : List File = RequestError String
, filter : String | Loading
, error : Maybe String | Files (List TaskStatus.Status)
}
initModel : Model initModel : Model
initModel = initModel = Loading
{ files =
[]
, filter = ""
, error = Nothing
}
init : ( Model, Cmd Msg ) init : ( Model, Cmd Msg )
@@ -56,26 +41,18 @@ init =
type Msg type Msg
= Reload = Reload
| QueryRequestResult (Result Http.Error FilesResponse) | QueryRequestResult (Result Http.Error FilesResponse)
| TaskStatusMsg TaskStatus.Msg
-- Request -- -- Request --
type alias FilesResponse =
{ files : List TaskStatus.Status }
fileDecoder : Decoder File
fileDecoder =
succeed File
|> required "name" string
|> required "file_name" string
|> required "source" string
|> required "id" int
|> required "status" string
queueDecoder : Decoder FilesResponse queueDecoder : Decoder FilesResponse
queueDecoder = queueDecoder =
succeed FilesResponse succeed FilesResponse
|> required "queue" (list fileDecoder) |> required "files" (list TaskStatus.statusDecoder)
@@ -86,10 +63,13 @@ update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = update msg model =
case msg of case msg of
QueryRequestResult (Ok res) -> QueryRequestResult (Ok res) ->
( { model | files = res.queue }, Cmd.none ) ( Files res.files, Cmd.none )
QueryRequestResult (Err (Http.BadBody er)) -> QueryRequestResult (Err (Http.BadBody err)) ->
( { model | error = Just er }, Cmd.none ) ( RequestError err, Cmd.none )
TaskStatusMsg sub_msg ->
( model, Cmd.map TaskStatusMsg <| TaskStatus.update sub_msg)
_ -> _ ->
( model, Cmd.none ) ( model, Cmd.none )
@@ -127,62 +107,6 @@ cardShadowOver =
} }
viewFile : File -> Element Msg
viewFile file =
el
[ width fill
-- , Background.color (rgb 0.8 0.8 0.8)
, padding 10
, Border.rounded 3
, cardShadow
, height (px 110)
, mouseOver [ cardShadowOver ]
]
(Element.column
[ width fill
, spacing 6
]
[ Element.row
[ width fill ]
[ el
[ Region.heading 1
, alignLeft
, Font.size 24
]
(text file.name)
, el
[ Region.heading 1
, alignRight
, Font.color (rgba 0 0 0 0.6)
, Font.size 20
]
(text (String.concat [ "[", file.status, "]" ]))
]
, el
[ Region.heading 2
, Font.size 16
, spacingXY 0 10
]
(text (String.concat [ "File name: ", file.file_name ]))
, el
[ Region.heading 2
, Font.size 16
]
(text (String.concat [ "Source: ", file.source ]))
, download
[ Region.footer
, Font.color (rgb 0 0 0.8)
, Font.size 16
, alignBottom
, alignRight
]
{ url = String.concat [ "http://127.0.0.1/api/file/", toString file.id ]
, label = text "Download"
}
]
)
view : Model -> Element Msg view : Model -> Element Msg
view model = view model =
@@ -200,19 +124,26 @@ view model =
, Font.size 36 , Font.size 36
] ]
(text "Files") (text "Files")
, case model.error of , case model of
Just e -> Loading ->
el el
[ Font.color (rgb 1 0 0) [ Font.color (rgb 1 0 0)
, Font.size 16 , Font.size 16
] ]
(text e) (text "Loading")
Nothing -> RequestError err ->
el
[ Font.color (rgb 1 0 0)
, Font.size 16
]
(text <| String.concat ["Loading", err])
Files files ->
Element.column Element.column
[ width fill [ width fill
, centerX , centerX
, spacing 20 , spacing 20
] ]
(List.map viewFile model.files) ( files |> List.map TaskStatus.statusView |> List.map (Element.map TaskStatusMsg) )
] ]

View File

@@ -3,7 +3,7 @@ module Globals exposing (..)
api : String api : String
api = api =
"http://127.0.0.1/api/" "/api/"
apiEndpoint : String -> String apiEndpoint : String -> String
@@ -14,6 +14,9 @@ apiEndpoint path =
apiStatus : String -> String apiStatus : String -> String
apiStatus id = String.concat [apiEndpoint "status/", id] apiStatus id = String.concat [apiEndpoint "status/", id]
apiAdd : String
apiAdd = apiEndpoint "add"
apiDownloadFile : String -> String apiDownloadFile : String -> String
apiDownloadFile id = String.concat [apiEndpoint "file/", id] apiDownloadFile id = String.concat [apiEndpoint "file/", id]

View File

@@ -13,7 +13,8 @@ import Http
import Json.Decode exposing (Decoder, int, string, succeed) import Json.Decode exposing (Decoder, int, string, succeed)
import Json.Decode.Pipeline exposing (required) import Json.Decode.Pipeline exposing (required)
import Json.Encode as Encode import Json.Encode as Encode
import Html.Events exposing (onMouseOver)
import Globals exposing(apiAdd)
type alias Form = type alias Form =
{ url : String { url : String
@@ -43,7 +44,7 @@ type Msg
| UpdateFormFormatChnaged Format | UpdateFormFormatChnaged Format
| PostForm Form | PostForm Form
| PostResult (Result Http.Error PostFormResponse) | PostResult (Result Http.Error PostFormResponse)
| None
type AudioFormats type AudioFormats
@@ -178,7 +179,7 @@ update navKey msg model =
if not (String.isEmpty form.url) then if not (String.isEmpty form.url) then
( model ( model
, Http.post , Http.post
{ url = "http://127.0.0.1/api/add" { url = apiAdd
, body = Http.jsonBody (formPostEncoder form) , body = Http.jsonBody (formPostEncoder form)
, expect = Http.expectJson PostResult formDecoder , expect = Http.expectJson PostResult formDecoder
} }
@@ -286,6 +287,7 @@ formatButton position label state =
, Border.widthEach borders , Border.widthEach borders
, Border.color color.blue , Border.color color.blue
, width (px 80) , width (px 80)
, mouseOver [ Background.color color.lightBlue ]
, Background.color <| , Background.color <|
if state == Input.Selected then if state == Input.Selected then
color.lightBlue color.lightBlue
@@ -370,6 +372,7 @@ view model =
, Border.color color.darkBlue , Border.color color.darkBlue
, paddingXY 32 16 , paddingXY 32 16
, Border.rounded 3 , Border.rounded 3
, mouseOver [ Background.color color.darkBlue ]
] ]
{ onPress = Just (PostForm model.form) { onPress = Just (PostForm model.form)
, label = Element.text "Download" , label = Element.text "Download"

View File

@@ -1,22 +1,16 @@
module Status exposing (Model, Msg, init, queryStatus, update, view) module Status exposing (Model, Msg, init, queryStatus, update, view)
import Color exposing (color)
import Debug exposing (toString)
import Delay import Delay
import Element exposing (..) import Element exposing (..)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import File.Download exposing (url)
import Files exposing (Msg(..)) import Files exposing (Msg(..))
import Globals exposing (apiDownloadFile, apiStatus) import Globals exposing (apiStatus)
import Http import Http
import Json.Decode exposing (Decoder, int, list, string, succeed) import Json.Decode exposing (Decoder, succeed)
import Json.Decode.Pipeline exposing (optional, required) import Json.Decode.Pipeline exposing (required)
import Route exposing (Route(..)) import Route exposing (Route(..))
import String exposing (right)
import Task import Task
import TaskStatus
@@ -26,7 +20,7 @@ import Task
type Msg type Msg
= Reload = Reload
| StatusRequestResult (Result Http.Error StatusResponse) | StatusRequestResult (Result Http.Error StatusResponse)
| Download String | TaskStatusMsg TaskStatus.Msg
@@ -35,7 +29,7 @@ type Msg
type alias Model = type alias Model =
{ file_id : String { file_id : String
, status : Maybe Status , status : Maybe TaskStatus.Status
} }
@@ -47,55 +41,16 @@ init file_id =
-- Request -- Request
type alias File =
{ format : String
, filename : String
, timestamp : Int
}
type alias Status =
{ file : Maybe File
, name : String
, source : String
, status : String
, status_id : Int
, format : String
, timestamp : Int
, id : String
}
type alias StatusResponse = type alias StatusResponse =
{ status : Status } { status : TaskStatus.Status }
fileDecoder : Decoder File
fileDecoder =
succeed File
|> required "format" string
|> required "name" string
|> required "timestamp" int
statusDecoder : Decoder Status
statusDecoder =
succeed Status
|> optional "file" (Json.Decode.map Just fileDecoder) Nothing
|> required "name" string
|> required "source" string
|> required "status" string
|> required "status_id" int
|> required "task_file_type" string
|> required "timestamp" int
|> required "id" string
statusResponseDecoder : Decoder StatusResponse statusResponseDecoder : Decoder StatusResponse
statusResponseDecoder = statusResponseDecoder =
succeed StatusResponse succeed StatusResponse
|> required "status" statusDecoder |> required "status" TaskStatus.statusDecoder
queryStatus : String -> Cmd Msg queryStatus : String -> Cmd Msg
@@ -110,48 +65,6 @@ queryStatus id =
-- Update -- Update
cardShadow : Attr decorative msg
cardShadow =
Border.shadow
{ offset = ( 0, 4 )
, size = 4
, blur = 8
, color = rgba 0 0 0 0.2
}
cardShadowOver : Attr decorative msg
cardShadowOver =
Border.shadow
{ offset = ( 0, 4 )
, size = 8
, blur = 16
, color = rgba 0 0 0 0.2
}
card : List (Attribute msg) -> Element msg -> Element msg
card attr element =
el
(List.concat
[ attr
, [ Border.rounded 15, cardShadow, mouseOver [ cardShadowOver ], padding 20 ]
]
)
element
innerCard : List (Attribute msg) -> Element msg -> Element msg
innerCard attr element =
el
(List.concat
[ attr
, [ Border.roundEach { topLeft = 15, topRight = 15, bottomLeft = 15, bottomRight = 15 }, padding 5 ]
]
)
element
send : msg -> Cmd msg send : msg -> Cmd msg
send msg = send msg =
Task.succeed msg Task.succeed msg
@@ -170,48 +83,11 @@ update msg model =
StatusRequestResult _ -> StatusRequestResult _ ->
( { model | status = Nothing }, Cmd.none ) ( { model | status = Nothing }, Cmd.none )
Download file_id -> TaskStatusMsg sub_msg ->
( model, url <| apiDownloadFile file_id ) ( model, Cmd.map TaskStatusMsg <| TaskStatus.update sub_msg )
-- View
--Just (Download.
statusView : Status -> Element Msg
statusView status =
card [ width fill, height shrink ]
(Element.column
[ width fill ]
[ row [ width fill, height <| px 40 ]
[ el [ Font.size 28, alignLeft ] <| text status.name
, el [ Font.size 28, alignRight ] <| text (String.concat [ "[", status.status, "]" ])
]
, link [ Font.bold, Font.underline, Font.color color.blue, Font.size 16, padding 2 ]
{ url = status.source, label = text status.source }
, el [ height <| px 10 ] none
, case status.file of
Just file ->
innerCard [ width fill, height shrink, Background.color color.grey ] <|
row [ width fill, spacing 20 ]
[ Input.button
[ Background.color color.blue
, Font.color color.white
, Border.color color.red
, paddingXY 32 16
, Border.rounded 15
]
{ onPress = Just <| Download status.id
, label = Element.text "Download"
}
, el [ Font.size 16 ] <| text file.filename
]
Nothing ->
Element.none
]
)
view : Model -> Element Msg view : Model -> Element Msg
@@ -225,8 +101,8 @@ view model =
] ]
[ case model.status of [ case model.status of
Just status -> Just status ->
statusView status TaskStatus.statusView status |> Element.map TaskStatusMsg
Nothing -> Nothing ->
Element.text "Test" Element.text "File Not Found"
] ]

View File

@@ -0,0 +1,132 @@
module TaskStatus exposing (..)
import Json.Decode exposing (Decoder, int, list, string, succeed)
import Json.Decode.Pipeline exposing (optional, required)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Element exposing (..)
import String
import Globals exposing (apiDownloadFile)
import File.Download exposing (url)
import Color exposing (color)
type alias File =
{ format : String
, filename : String
, timestamp : Int
}
type alias Status =
{ file : Maybe File
, name : String
, source : String
, status : String
, timestamp : Int
, id : String
}
fileDecoder : Decoder File
fileDecoder =
succeed File
|> required "format" string
|> required "filename" string
|> required "timestamp" int
statusDecoder : Decoder Status
statusDecoder =
succeed Status
|> optional "file" (Json.Decode.map Just fileDecoder) Nothing
|> required "name" string
|> required "source" string
|> required "status" string
|> required "timestamp" int
|> required "hash" string
type Msg
= Download String
update : Msg -> Cmd Msg
update msg =
case msg of
Download id -> url <| apiDownloadFile id
statusView : Status -> Element Msg
statusView status =
card [ width fill, height shrink ]
(Element.column
[ width fill ]
[ row [ width fill, height <| px 40 ]
[ el [ Font.size 28, alignLeft ] <| text status.name
, el [ Font.size 28, alignRight ] <| text (String.concat [ "[", status.status, "]" ])
]
, link [ Font.bold, Font.underline, Font.color color.blue, Font.size 16, padding 2 ]
{ url = status.source, label = text status.source }
, el [ height <| px 10 ] none
, case status.file of
Just file ->
innerCard [ width fill, height shrink, Background.color color.grey ] <|
row [ width fill, spacing 20 ]
[ Input.button
[ Background.color color.blue
, Font.color color.white
, Border.color color.red
, paddingXY 32 16
, Border.rounded 15
]
{ onPress = Just <| Download status.id
, label = Element.text "Download"
}
, el [ Font.size 16 ] <| text file.filename
]
Nothing ->
Element.none
]
)
cardShadow : Attr decorative msg
cardShadow =
Border.shadow
{ offset = ( 0, 4 )
, size = 4
, blur = 8
, color = rgba 0 0 0 0.2
}
cardShadowOver : Attr decorative msg
cardShadowOver =
Border.shadow
{ offset = ( 0, 4 )
, size = 8
, blur = 16
, color = rgba 0 0 0 0.2
}
card : List (Attribute msg) -> Element msg -> Element msg
card attr element =
el
(List.concat
[ attr
, [ Border.rounded 15, cardShadow, mouseOver [ cardShadowOver ], padding 20 ]
]
)
element
innerCard : List (Attribute msg) -> Element msg -> Element msg
innerCard attr element =
el
(List.concat
[ attr
, [ Border.roundEach { topLeft = 15, topRight = 15, bottomLeft = 15, bottomRight = 15 }, padding 5 ]
]
)
element

22
conanfile.txt Normal file
View File

@@ -0,0 +1,22 @@
[requires]
boost/1.76.0
soci/4.0.2
fmt/7.1.3
spdlog/1.8.5
cpp-httplib/0.8.8
magic_enum/0.7.2
ctre/3.4.1
nlohmann_json/3.9.1
[generators]
cmake
[options]
soci:shared=True
soci:with_boost=True
soci:with_sqlite3=True
[imports]
lib, *.so* -> ./bin