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/.vscode/settings.json
build/
.vscode/settings.json

View File

@@ -1,5 +1,8 @@
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})
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
endif()
@@ -7,14 +10,21 @@ message("using vcpkg toolchain: ${CMAKE_TOOLCHAIN_FILE}" )
project(localTube)
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)
find_package(soci CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(Boost COMPONENTS system filesystem REQUIRED)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()
endif()
add_subdirectory(Modules)

View File

@@ -5,13 +5,18 @@ set(src_files
src/api.cpp
src/worker.hpp
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)
set(LIBS ${CONAN_LIBS})
else()
set(LIBS
fmt::fmt
spdlog::spdlog
SOCI::soci_core
@@ -21,3 +26,8 @@ target_link_libraries(.
Boost::boost
${Boost_FILESYSTEM_LIBRARY}
${Boost_SYSTEM_LIBRARY})
endif()
target_link_libraries(localTubeServer ${LIBS})

View File

@@ -7,6 +7,7 @@
#include <filesystem>
#include <fstream>
#include <functional>
#include <exception>
#include <soci/boost-optional.h>
@@ -14,9 +15,10 @@
#include <fmt/chrono.h>
#include <fmt/ostream.h>
#include <nlohmann/json.hpp>
#include <openssl/md5.h>
#include <spdlog/spdlog.h>
#include "file_status.hpp"
using json = nlohmann::json;
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
{
auto data = TaskInfo::getAll(sql);
json res;
// 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);
}
res["files"] = data;
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);
std::ifstream fs(dw.local_path, std::ios_base::binary);
fs.seekg(0, std::ios_base::end);
auto size = fs.tellg();
fs.seekg(0);
rs.body.resize(static_cast<size_t>(size));
fs.read(&rs.body[0], static_cast<std::streamsize>(size));
auto fs =
std::make_shared<std::ifstream>(dw.local_path, std::ios_base::binary | std::ios::in);
fs->seekg(0, std::ios_base::end);
auto size = fs->tellg();
fs->seekg(0);
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)
{
@@ -221,55 +194,27 @@ void Api::status(httplib::Request const& rq, httplib::Response& rs)
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;
auto& status = response["status"];
try
{
auto data = TaskInfo::getId(sql, hash);
if (data)
{
auto const& el = data.value();
status["name"] = fmt::format("Task {}", el.hash);
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;
sql << "SELECT * FROM Files WHERE hash=:hash", soci::use(hash, "hash"),
soci::into(file_data);
if (file_data)
{
auto const& file = file_data.value();
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);
}
}
response["status"] = data.value();
} else
{
rs.status = 404;
response["error"] = fmt::format("File '{}' not found!", hash);
}
}
catch (std::exception const& e)
{
spdlog::error("[Api Status] Excepion: {}", e.what());
rs.status = 404;
response["error"] = fmt::format("Exception: {}", e.what());
}
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;
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);
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 += "--restrict-filenames";
args += task.url;
/* ** Download ** */
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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,32 @@
module Files exposing (..)
import Debug exposing (toString)
import String
import Element exposing (..)
import Element.Border as Border
import Element.Font as Font
import Element.Region as Region
import Globals exposing (apiFiles)
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 TaskStatus
type alias File =
{ name : String
, file_name : String
, source : String
, id : Int
, status : String
}
type alias FilesResponse =
{ queue : List File }
-- Model --
type alias Model =
{ files : List File
, filter : String
, error : Maybe String
}
type Model
= RequestError String
| Loading
| Files (List TaskStatus.Status)
initModel : Model
initModel =
{ files =
[]
, filter = ""
, error = Nothing
}
initModel = Loading
init : ( Model, Cmd Msg )
@@ -56,26 +41,18 @@ init =
type Msg
= Reload
| QueryRequestResult (Result Http.Error FilesResponse)
| TaskStatusMsg TaskStatus.Msg
-- Request --
fileDecoder : Decoder File
fileDecoder =
succeed File
|> required "name" string
|> required "file_name" string
|> required "source" string
|> required "id" int
|> required "status" string
type alias FilesResponse =
{ files : List TaskStatus.Status }
queueDecoder : Decoder FilesResponse
queueDecoder =
succeed FilesResponse
|> required "queue" (list fileDecoder)
|> required "files" (list TaskStatus.statusDecoder)
@@ -86,10 +63,13 @@ update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
QueryRequestResult (Ok res) ->
( { model | files = res.queue }, Cmd.none )
( Files res.files, Cmd.none )
QueryRequestResult (Err (Http.BadBody er)) ->
( { model | error = Just er }, Cmd.none )
QueryRequestResult (Err (Http.BadBody err)) ->
( RequestError err, Cmd.none )
TaskStatusMsg sub_msg ->
( model, Cmd.map TaskStatusMsg <| TaskStatus.update sub_msg)
_ ->
( 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 =
@@ -200,19 +124,26 @@ view model =
, Font.size 36
]
(text "Files")
, case model.error of
Just e ->
, case model of
Loading ->
el
[ Font.color (rgb 1 0 0)
, 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
[ width fill
, centerX
, 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 =
"http://127.0.0.1/api/"
"/api/"
apiEndpoint : String -> String
@@ -14,6 +14,9 @@ apiEndpoint path =
apiStatus : String -> String
apiStatus id = String.concat [apiEndpoint "status/", id]
apiAdd : String
apiAdd = apiEndpoint "add"
apiDownloadFile : String -> String
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.Pipeline exposing (required)
import Json.Encode as Encode
import Html.Events exposing (onMouseOver)
import Globals exposing(apiAdd)
type alias Form =
{ url : String
@@ -43,7 +44,7 @@ type Msg
| UpdateFormFormatChnaged Format
| PostForm Form
| PostResult (Result Http.Error PostFormResponse)
| None
type AudioFormats
@@ -178,7 +179,7 @@ update navKey msg model =
if not (String.isEmpty form.url) then
( model
, Http.post
{ url = "http://127.0.0.1/api/add"
{ url = apiAdd
, body = Http.jsonBody (formPostEncoder form)
, expect = Http.expectJson PostResult formDecoder
}
@@ -286,6 +287,7 @@ formatButton position label state =
, Border.widthEach borders
, Border.color color.blue
, width (px 80)
, mouseOver [ Background.color color.lightBlue ]
, Background.color <|
if state == Input.Selected then
color.lightBlue
@@ -370,6 +372,7 @@ view model =
, Border.color color.darkBlue
, paddingXY 32 16
, Border.rounded 3
, mouseOver [ Background.color color.darkBlue ]
]
{ onPress = Just (PostForm model.form)
, label = Element.text "Download"

View File

@@ -1,22 +1,16 @@
module Status exposing (Model, Msg, init, queryStatus, update, view)
import Color exposing (color)
import Debug exposing (toString)
import Delay
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 Globals exposing (apiDownloadFile, apiStatus)
import Globals exposing (apiStatus)
import Http
import Json.Decode exposing (Decoder, int, list, string, succeed)
import Json.Decode.Pipeline exposing (optional, required)
import Json.Decode exposing (Decoder, succeed)
import Json.Decode.Pipeline exposing (required)
import Route exposing (Route(..))
import String exposing (right)
import Task
import TaskStatus
@@ -26,7 +20,7 @@ import Task
type Msg
= Reload
| StatusRequestResult (Result Http.Error StatusResponse)
| Download String
| TaskStatusMsg TaskStatus.Msg
@@ -35,7 +29,7 @@ type Msg
type alias Model =
{ file_id : String
, status : Maybe Status
, status : Maybe TaskStatus.Status
}
@@ -47,55 +41,16 @@ init file_id =
-- 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 =
{ 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 =
succeed StatusResponse
|> required "status" statusDecoder
|> required "status" TaskStatus.statusDecoder
queryStatus : String -> Cmd Msg
@@ -110,48 +65,6 @@ queryStatus id =
-- 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 =
Task.succeed msg
@@ -170,48 +83,11 @@ update msg model =
StatusRequestResult _ ->
( { model | status = Nothing }, Cmd.none )
Download file_id ->
( model, url <| apiDownloadFile file_id )
TaskStatusMsg sub_msg ->
( 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
@@ -225,8 +101,8 @@ view model =
]
[ case model.status of
Just status ->
statusView status
TaskStatus.statusView status |> Element.map TaskStatusMsg
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