Compare commits
15 Commits
d4c935fd21
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
f42985733f
|
|||
|
c6cc83018f
|
|||
|
89cb0c475b
|
|||
|
95dbb74b47
|
|||
|
a894717e52
|
|||
|
1092860b42
|
|||
|
ec5daba300
|
|||
|
3800b448e1
|
|||
|
4a6bb957a1
|
|||
|
666f87a5f3
|
|||
|
06e12c186a
|
|||
|
e0d0bef292
|
|||
|
004dd7d161
|
|||
|
dc5460af0d
|
|||
|
37a7cbba6e
|
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
Dockerfile
|
||||
target
|
||||
5
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
/dist
|
||||
target
|
||||
dist
|
||||
.env
|
||||
|
||||
1855
Cargo.lock
generated
23
Cargo.toml
@ -1,20 +1,3 @@
|
||||
[package]
|
||||
name = "singit2"
|
||||
version = "0.2.0"
|
||||
authors = ["Joakim Hulthe <joakim@hulthe.net"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
seed = "0.8.0"
|
||||
#wasm-bindgen = "0.2.70"
|
||||
serde = { version = "1", features = ['derive'] }
|
||||
serde_json = "1"
|
||||
anyhow = "*"
|
||||
rand = "0.8.4"
|
||||
|
||||
[dependencies.css_typegen]
|
||||
git = "https://github.com/hulthe/css_typegen.git"
|
||||
branch = "master"
|
||||
[workspace]
|
||||
members = ["backend", "frontend"]
|
||||
resolver = "2"
|
||||
|
||||
76
Dockerfile
Normal file
@ -0,0 +1,76 @@
|
||||
##################
|
||||
### BASE STAGE ###
|
||||
##################
|
||||
FROM rust:1.72.1 as base
|
||||
|
||||
# Install build dependencies
|
||||
RUN cargo install --locked trunk strip_cargo_version
|
||||
RUN rustup target add wasm32-unknown-unknown
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
WORKDIR /app
|
||||
RUN mkdir frontend backend common
|
||||
|
||||
###########################
|
||||
### STRIP-VERSION STAGE ###
|
||||
###########################
|
||||
FROM base AS strip-version
|
||||
|
||||
COPY Cargo.lock Cargo.toml ./
|
||||
COPY frontend/Cargo.toml ./frontend/
|
||||
COPY backend/Cargo.toml ./backend/
|
||||
#COPY common/Cargo.toml ./common/
|
||||
RUN strip_cargo_version
|
||||
|
||||
###################
|
||||
### BUILD STAGE ###
|
||||
###################
|
||||
FROM base AS build
|
||||
|
||||
RUN cargo init --lib frontend
|
||||
RUN cargo init --bin backend
|
||||
RUN cargo init --lib common
|
||||
|
||||
COPY --from=strip-version /app/frontend/Cargo.toml /app/frontend/
|
||||
COPY --from=strip-version /app/backend/Cargo.toml /app/backend/
|
||||
#COPY --from=strip-version /app/common/Cargo.toml /app/common/
|
||||
COPY --from=strip-version /app/Cargo.toml /app/Cargo.lock /app/
|
||||
|
||||
WORKDIR /app/backend
|
||||
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
WORKDIR /app/frontend
|
||||
RUN cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
WORKDIR /app/backend
|
||||
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
WORKDIR /app/frontend
|
||||
RUN trunk build --release
|
||||
|
||||
########################
|
||||
### PRODUCTION STAGE ###
|
||||
########################
|
||||
FROM scratch
|
||||
|
||||
# Default logging level
|
||||
ENV RUST_LOG="info"
|
||||
|
||||
ENV COVERS_DIR="/covers"
|
||||
VOLUME /covers
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Copy application binary
|
||||
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/singit_srv /usr/local/bin/singit_srv
|
||||
|
||||
# Copy static web files
|
||||
COPY --from=build /app/frontend/dist /dist
|
||||
|
||||
# Copy database migrations
|
||||
COPY backend/migrations /migrations
|
||||
|
||||
ENTRYPOINT ["singit_srv"]
|
||||
1
backend/.env.example
Normal file
@ -0,0 +1 @@
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
2014
backend/Cargo.lock
generated
Normal file
17
backend/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "singit_srv"
|
||||
version = "1.0.0"
|
||||
authors = ["Joakim Hulthe <joakim@hulthe.net"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6.2"
|
||||
actix-web = { version = "4.4.0", default-features = false, features = ["macros"] }
|
||||
clap = { version = "4.4.4", features = ["derive", "env"] }
|
||||
diesel = "2.1.1"
|
||||
diesel-async = { version = "0.4.1", features = ["postgres", "deadpool"] }
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.10.0"
|
||||
eyre = "0.6.8"
|
||||
log = "0.4.20"
|
||||
serde = { version = "1.0.188", default-features = false, features = ["derive", "std"] }
|
||||
9
backend/diesel.toml
Normal file
@ -0,0 +1,9 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
1
backend/dist
Symbolic link
@ -0,0 +1 @@
|
||||
../frontend/dist
|
||||
0
backend/migrations/.keep
Normal file
@ -0,0 +1,6 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
||||
@ -0,0 +1,36 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@ -0,0 +1,2 @@
|
||||
DROP TABLE song;
|
||||
|
||||
13
backend/migrations/2023-09-21-174215_add_song_table/up.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE TABLE song(
|
||||
song_hash TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
artist TEXT NOT NULL,
|
||||
cover TEXT,
|
||||
language TEXT,
|
||||
video TEXT,
|
||||
year TEXT,
|
||||
genre TEXT,
|
||||
bpm TEXT NOT NULL,
|
||||
duet_singer_1 TEXT,
|
||||
duet_singer_2 TEXT
|
||||
);
|
||||
@ -0,0 +1,2 @@
|
||||
DROP TABLE custom_list_entry;
|
||||
DROP TABLE custom_list;
|
||||
10
backend/migrations/2023-09-21-202738_add_custom_lists/up.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE custom_list (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE custom_list_entry (
|
||||
list_id SERIAL REFERENCES custom_list(id),
|
||||
song_hash TEXT NOT NULL REFERENCES song(song_hash),
|
||||
PRIMARY KEY (list_id, song_hash)
|
||||
);
|
||||
253
backend/mock/custom_lists.sql
Normal file
@ -0,0 +1,253 @@
|
||||
insert into custom_list(id, name) VALUES (1, 'tux');
|
||||
insert into custom_list_entry(list_id, song_hash) VALUES
|
||||
(1, '415e7f2a9ca15306f462493dea011328'),
|
||||
(1, 'c2ada59d4465e891565ed6480c95402b'),
|
||||
(1, 'b64e1a6f02bc3dbc9cf42ec51465f8a4'),
|
||||
(1, '61611e2ae7da9f5fa307c677aa768749'),
|
||||
(1, '331b7741323bb299558d2889662bc90c'),
|
||||
(1, '83c8eb8a644c01085406611a54c746b3'),
|
||||
(1, '5305707bae7a4072d6f72d2c83c6fe19'),
|
||||
(1, '2a405748c627c30bfdf029361d09618c'),
|
||||
(1, '24bf0d992eb9bfe83f0355574e52cd66'),
|
||||
(1, '128d3a0b6109b61e7c5266942e04e45a'),
|
||||
(1, '4cda857d6a4d1365a98f0f4b0f6292da'),
|
||||
(1, '8fd5f78019ef7cdfa595d5c51a147ba7'),
|
||||
(1, '138a95285762edcfb7916b9aef329083'),
|
||||
(1, '8e9c0d935b93569858817a4ae9513875'),
|
||||
(1, 'b865f24ced688b6932c6131d644f7975'),
|
||||
(1, 'f83893a4322289818328b42468728314'),
|
||||
(1, '92d04dbea98658cd07d9b2c29356ab52'),
|
||||
(1, '9e2975f1cb67bd0ff76c83e9d2b3c7a4'),
|
||||
(1, 'cf9eaf021b3d78cd2491be771abbe13b'),
|
||||
(1, '3a04e1eec93c224af777c7f951dd2d98'),
|
||||
(1, 'b20b9c8f2bdb43086ec9d46b9a6bada0'),
|
||||
(1, '85ad23ce6d40d27656a9cbd54e3f6ab4'),
|
||||
(1, 'ab32d01ba3d2a557c51c7b26f08f61f0'),
|
||||
(1, '989118dd28898b84962858ec44e2f7e2'),
|
||||
(1, 'bf7a666e3d37a1d7388ed838bb6f020a'),
|
||||
(1, 'd30cf94ad68c50f660a31bcf637c76bd'),
|
||||
(1, '053161d0a98c27ea4a9ee5e0e404e848'),
|
||||
(1, 'c9cf0764a339f79b783087db605fe8f0'),
|
||||
(1, 'd5c4c3513cb8869643f42e1350474ad3'),
|
||||
(1, '0a8a99dc8ffafaf837f0755fa2e54009'),
|
||||
(1, 'fcea59940c64e1257ce9264254ba8e03'),
|
||||
(1, '63c18435bbeef45e6fafc947ca5b1ff5'),
|
||||
(1, '6620e5b50f4f1ee7358bbe0d609edfe2'),
|
||||
(1, '265a22dc97ee56e2e1a4ce525f3e79c2'),
|
||||
(1, '9adb1f7ec660e0f2643cd151fad69b82'),
|
||||
(1, '6443a7047288aab14ee4694ebe87b79b'),
|
||||
(1, '80a356c273b7d52049a23e91e36aa9f0'),
|
||||
(1, '0f73fa30f7f20d5235dcac8eabff472d'),
|
||||
(1, '5a4b69e047a949744a81715a83f20ee3'),
|
||||
(1, '3407c6f6ee7b1970c094b0c3144bf54a'),
|
||||
(1, '9a4fbaee92151f345680abe2fc9ee747'),
|
||||
(1, '1a049f2fe3a59d840c5157600c8743b5'),
|
||||
(1, 'd6f4b7dd36e9ddb4e654d2d6836ff15e'),
|
||||
(1, 'be090ec6b8d3dcd206c29c2620b673d6'),
|
||||
(1, '2de6e630dc402b3f1dd565e25376fa6e'),
|
||||
(1, '8527cc85dfb71405e0c509d60d530fa1'),
|
||||
(1, '495b964e442823eb695e6e73d59ed100'),
|
||||
(1, '09bfeb3ac6fc284b0ef8ab47c055e2b0'),
|
||||
(1, 'a5f09a3e3aae08d5576d8c24656ea1da'),
|
||||
(1, 'e7110b6577a733df0ef96559c72fdc3a'),
|
||||
(1, '61a8dacb968a51d010af72cd9ba0fcc7'),
|
||||
(1, '126c7aa03977ba6179c14d1286d95987'),
|
||||
(1, '7bafa94e8b192112fcc13285733fed21'),
|
||||
(1, '55efe418039d6b4c1a0a3033a505a6d5'),
|
||||
(1, 'd6cd321817e8d2ed7ebb2a7282d7ba08'),
|
||||
(1, '56f7f540f406a8f4feabdc976ffc02a5'),
|
||||
(1, '5bd19f7edc9a5aceb165bf982777fbef'),
|
||||
(1, 'e8f3382284dd8f3c14f74e543cbe1365'),
|
||||
(1, '8b1bef88f60a945f23695a98ab461a32'),
|
||||
(1, '34340558fe5254ec8d632b28412c1df6'),
|
||||
(1, '133c04455080e1e700ea9168eb6e9154'),
|
||||
(1, '40b939fea1cc7bca69f94ba98a9a8966'),
|
||||
(1, '17110ec3c9490c2d7212bac578178f82'),
|
||||
(1, '4013a6b73babbe4ddb7c94308f607981'),
|
||||
(1, '606aa3c2fd587ba49e06e2ba96634d3e'),
|
||||
(1, 'f3ddf171f30f886bb65f2eb9b5e9b631'),
|
||||
(1, 'b441c2178c3ea7b470905972a8542a45'),
|
||||
(1, '8c0bcc859b936e07125a8a1496fc5cff'),
|
||||
(1, '37903e02cd6f096b7c18130ef322f245'),
|
||||
(1, '246a6ba918b31591b8a3bb39e89e57ca'),
|
||||
(1, 'df864a07605f68f58d454991035887ad'),
|
||||
(1, '4407108e5fcce7999b4ed6a02235829e'),
|
||||
(1, '8504e0862ec12cca41caa1f78681ec03'),
|
||||
(1, '75a3f6fbf821d473bf17025e043a5964'),
|
||||
(1, '0bc6e109374f04db38ec70d292371d19'),
|
||||
(1, '5fb96e336c571545a9e3a1afa0ce25c2'),
|
||||
(1, 'def1a11db7a5899c9b0ba357fc71a37f'),
|
||||
(1, '581e463cee62d8adafd84b13deeb507d'),
|
||||
(1, '503a857d480a4ee4c30b132b80e895d1'),
|
||||
(1, 'cf60d6a32e7a1256f1e69b518fe158ff'),
|
||||
(1, '980817c5a574eb50f3ab26f390b84b4f'),
|
||||
(1, '2aa8f43167062239fa955a6fd5bf209b'),
|
||||
(1, '16318fbae71adefaf80991849a181d11'),
|
||||
(1, '43fd446c68528a56931f755525b8bafc'),
|
||||
(1, 'abf514722acd55a48f5c1ec8badd899d'),
|
||||
(1, 'fb3dd61449de05c2b84d3d75cf346d81'),
|
||||
(1, '55a069e51045adab0adf4d81ca36011e'),
|
||||
(1, '5694db971c54e0872defef4a300343f9'),
|
||||
(1, 'd67f527b382394095a3ca40ac8449904'),
|
||||
(1, 'db52ed089f2008181515e3f8be54bf90'),
|
||||
(1, 'f2fffd34cef9a1878754640688487c82'),
|
||||
(1, 'd6ddd5009d865979febbfb36bc2d99b7'),
|
||||
(1, '362c2a15b7e8aaf8f1dfefd717fe7e29'),
|
||||
(1, 'b2c13e3d09706d5fde0243a44058f81c'),
|
||||
(1, '37d139647cca8827ad1205695a3daa33'),
|
||||
(1, 'c5732364b28b208d22e0f4cab71b5907'),
|
||||
(1, '236197f68795dea908d97fa61d8bb1c5'),
|
||||
(1, 'f54e7bc914d7c5fab66b371399acfd41'),
|
||||
(1, 'c2563f5a17677676ec45a0d487a2a871'),
|
||||
(1, '08f30c15ce2135cdf575e71aa9fadee8'),
|
||||
(1, 'ebd1f487cdb42b7d444b23c68a79918b'),
|
||||
(1, 'ae2739faa6e4b4c9de6962071be9fec6'),
|
||||
(1, '29c04079f0a8cdd22d95ff605847ccce'),
|
||||
(1, '3a0ffffb65779005d55f8d8f3d0a7091'),
|
||||
(1, '14c06f589bb0a772387888641b52454b'),
|
||||
(1, 'e0f3f3e0afff4e71d8a2e3a959a8effa'),
|
||||
(1, '2b843c3751dde9cfadfd392cac900487'),
|
||||
(1, '8c876d8344b468cfb89a5cc91c4a5218'),
|
||||
(1, '15917f7b815dce661db415ee1c488813'),
|
||||
(1, '54fb8b43f54e36e9a6a546d3c45aa4cf'),
|
||||
(1, 'a9430cf8d75c891c6c27ca6750faee8e'),
|
||||
(1, 'eb50f5eee7f570e976ab423fb44edde9'),
|
||||
(1, 'ed48d149f7ffa48d8c7eed5270f48949'),
|
||||
(1, '6fa37a33a1baea4875c9e342f8091dc3'),
|
||||
(1, '9ce446680bea61864df9d9fec5d54a93'),
|
||||
(1, 'f7ea025406d2645f86da52dde64807d0'),
|
||||
(1, '114fb59b3fee8ebf8c36fd2bf7e59a55'),
|
||||
(1, 'c1729df7dc9452a496c7be7f043ce88d'),
|
||||
(1, '92db823664bfa36f7fdde309fe69eb26'),
|
||||
(1, 'df833bc0ae6fd919e0db280a8f0f5b9e'),
|
||||
(1, '7d88370d2be9063ef347351781e3ad38'),
|
||||
(1, '1dbcab635439ff5d3432407fedb94da1'),
|
||||
(1, 'c79b580b5ba99bdf4669a61379747740'),
|
||||
(1, 'ce3722d28640ab845396d37933c66b1a'),
|
||||
(1, 'dd55c007590f65a011febd5c5ec17262'),
|
||||
(1, 'bb51d1b9fb2ca91fab7e63066f797c97'),
|
||||
(1, '597e0dc97c39604cbac73a887f89bf94'),
|
||||
(1, '2ed7517269cece052f7cbe4d8ec6f50c'),
|
||||
(1, '9c4adad2919051e6fe586354b9af132b'),
|
||||
(1, '2f9cc67c261ee31a0ecdd154168a987a'),
|
||||
(1, 'bb1a82b02f0de5a90fcb44edd2cccdf9'),
|
||||
(1, '9ec7df7febfce66dc43941e2c259904b'),
|
||||
(1, '1fd38f24222630b30cca1978e2acdb85'),
|
||||
(1, '9d051739db959a5dfb5b98890b4e6ae6'),
|
||||
(1, '9ae26319e307759e6e27c85ddff8f5fa'),
|
||||
(1, '3e3ac2c042b18fcc7f9610f654778740'),
|
||||
(1, '92aeb6900e0e06603dd846889c5d8ded'),
|
||||
(1, '096d2a19ddf0911a66c4fe4b41f992c7'),
|
||||
(1, 'cde2d982ab2caee5f0715c341bcae991'),
|
||||
(1, 'aa545b027a6fa39e5c278fc366ad9016'),
|
||||
(1, '1c5b8e85a66a415e39d996b967f92e67'),
|
||||
(1, '1b4673e2376ecfa02bae442cdc5b3c90'),
|
||||
(1, '9e542ff1cb9fb17f5f7a60af1fe9e66b'),
|
||||
(1, '8aa0b348759b2c56e17074b65a50f04a'),
|
||||
(1, '4931f242aeb4b1b641d813c6a2fbfadd'),
|
||||
(1, '2e3cfa5057486d0759cee8ee477a1678'),
|
||||
(1, '40444256345769c7a911e8710f0f5733'),
|
||||
(1, '54f85dd1796f5a8f5797c3965e8736f4'),
|
||||
(1, 'd8b2250234456105c3f1452afcffbfd5'),
|
||||
(1, '038e1f5299c2e7c4c87b54f655429666'),
|
||||
(1, '2ba74b69dc4671aa0fa705306c7cf072'),
|
||||
(1, 'fef173c794fec61b65089321673d0370'),
|
||||
(1, '366eff873b9665852f08b3d134dfd87c'),
|
||||
(1, '1bdc4632029c88612f481ab75ebc4062'),
|
||||
(1, 'b3acb1a516ee6949dd0f764200e80287'),
|
||||
(1, '00d59e67b764386960d8b423567cf9bc'),
|
||||
(1, 'b78fa5394c788492c5ddcfdbdfe4ae15'),
|
||||
(1, 'e8a7d9cb2aca242bf3f0c59640ddd620'),
|
||||
(1, '6059a22be14860cff767d679c98ed1df'),
|
||||
(1, 'b23ce25c9b6d789953b43d6a7f6f9266'),
|
||||
(1, '0499534b7a0545e81354fd6e814f4ad3'),
|
||||
(1, '77ea72cb592979d8aa617f1a06181600'),
|
||||
(1, '482de10e7a8d0688dd9ac27eef4cddb8'),
|
||||
(1, '570b9443265e8614befdb3c84bf66095'),
|
||||
(1, '5f572916554e6053ee9a59c2fbadb7c7'),
|
||||
(1, '79a9316e23134dc7632c51ec305af756'),
|
||||
(1, '4656a71e7802cff7757cc3e4a9281927'),
|
||||
(1, 'f986b0490311d17c909d8b8cceb6756d'),
|
||||
(1, '11c183d403657cafd9fe8f22921686cc'),
|
||||
(1, 'cbbf9ec130e723aa24e600d16471570b'),
|
||||
(1, 'e9a45f7858204bfc7b9ae196c3d9d7a7'),
|
||||
(1, '47097d94df0cb3b5f4c281e4d1077ce5'),
|
||||
(1, '09241a765e64d1f2280a888f255add7a'),
|
||||
(1, 'fb8f0d2dd52a205aa367d3f1b549f9c1'),
|
||||
(1, '97f260fceebdedcdc9f5ab99fef8a78e'),
|
||||
(1, '86a91d11239bc96df9c917c99bb19050'),
|
||||
(1, 'a1bac8f28c3378e56d0485cc880098d2'),
|
||||
(1, 'f563a6a7d1a20fc4dddca1dab785bd1f'),
|
||||
(1, '49159657f8644de0ea1c2dca29b11482'),
|
||||
(1, 'd94065d725bbdeedeebe1c5a8693873f'),
|
||||
(1, '03563f756de4a637b2bfa6c33b9ba3cd'),
|
||||
(1, '97b51c9df42b8e8754a6127073b6cf82'),
|
||||
(1, '865a527fc94bf609d2a86e6e7c2ca481'),
|
||||
(1, 'cb877f34c2a65534a458bf54ea823f08'),
|
||||
(1, '9d3b3d092d27b3ed0b76281225f98bf7'),
|
||||
(1, 'b27e8ad3b0d538abe256169cb3363b4a'),
|
||||
(1, '51c3819eb7ced02c46c22acda2c74205'),
|
||||
(1, '7ba6a2f608295d678aea4195b46a0219'),
|
||||
(1, 'db9785d69f04ab00266814b8e992da91'),
|
||||
(1, '2db8d58d0f3062f9d7e1259492652c1d'),
|
||||
(1, 'e2065c855899f8ed49e6ce89162683e8'),
|
||||
(1, '1319c9749c2ffdc6832e5aa98b54e659'),
|
||||
(1, 'fd34b1095c30bde6ce7aab8ec0683bb7'),
|
||||
(1, '7ad81d348b958714dea07ece9fd522c2'),
|
||||
(1, '2fb87de8954d03f91e6771846e2a8d6f'),
|
||||
(1, '6597cb67e75e5991ceb2afe448db3c35'),
|
||||
(1, '6c2619df4ae1e9377d47116d88eeeb5d'),
|
||||
(1, 'd25074818bd0a543482f55ff21330676'),
|
||||
(1, '8adf0a8c2be11dac6ec490bfebcc8d30'),
|
||||
(1, 'b1b6e78d3e26e1aed5a408ddb975547b'),
|
||||
(1, '1c8437de40bafcd6ba379ac53f1b8169'),
|
||||
(1, 'bb03494e04ca2cd3918de70f74498fcf'),
|
||||
(1, 'e00752cbe957fb707c6a6ac574452cd5'),
|
||||
(1, 'e1360e0256b6ce58c3f8591bb93a70dc'),
|
||||
(1, 'fd6c48fc6006b8276d12f8859daec350'),
|
||||
(1, '75b513f7b143b20a53c9a6d415bb02d1'),
|
||||
(1, 'ed2eff479f6e1333c83561125f8e64b9'),
|
||||
(1, '90a269caf890d1fae8fe57dc4ed19e56'),
|
||||
(1, 'b553b566dbb94a712641f346db436ad4'),
|
||||
(1, '76f9f24634d3590cbdc3dbfc2d84cb6c'),
|
||||
(1, '30c94206e5aac9e6936c42a2f60cc426'),
|
||||
(1, '692915e5a20c90c4c0d73f3db9c1154a'),
|
||||
(1, '836c2fa67a8d62c3b8c04bccd2ae31be'),
|
||||
(1, 'a514416bfa9ec0a46b98de23194398a4'),
|
||||
(1, '6abd9038780dd1f005df9fc31feb1560'),
|
||||
(1, 'e35237b0b1595f75b6e641669d67cfce'),
|
||||
(1, 'cbd15cc24b2892beb3444780fdbb19be'),
|
||||
(1, '26c91da37713481245b08bd74565f06b'),
|
||||
(1, '95cbbc0c9529592bee188dd2f3e7b8c7'),
|
||||
(1, 'f5d7f236ed25c850ec5e2f462eed7c4f'),
|
||||
(1, 'e5d7e266169ff20581413412b970c66c'),
|
||||
(1, 'e1e60351f1d5c9b62ad373e6343ccb78'),
|
||||
(1, '8c4a8899f75122dece210f306fc50de7'),
|
||||
(1, '70ec05034e36531f8cc15a995ee2a1ca'),
|
||||
(1, '1a9f9cbdf1adedcba49592fb48fb23f1'),
|
||||
(1, '1369618c25eb074761f2f9dd12cd082e'),
|
||||
(1, 'bcdd94a0c273aa1b0700de7e5e4213d0'),
|
||||
(1, '65248f0a7165ab82ee03e312f20526e7'),
|
||||
(1, '4c494372395a62e213ebecfe4dff643b'),
|
||||
(1, 'c270582f05844d3adef31a226386100f'),
|
||||
(1, '97047972139aa66b73ad3af35ca45b44'),
|
||||
(1, '40242bfc27f4b1a1af097bf88cc9619d'),
|
||||
(1, '604cabd06bd3b31fc5b8dd3802fcde2c'),
|
||||
(1, '687dc02afb37f19b29ceb2339eb52676'),
|
||||
(1, '581294986a59b3403d05de69c6be96d7'),
|
||||
(1, '38ec3bbef46ad213aef4731fe049f5f2'),
|
||||
(1, 'b50c594fc7e5603d5178b33109f6fb02'),
|
||||
(1, '7988cb4fd66dd5acf3c8e4ed87a1ec5f'),
|
||||
(1, '53a805ec4780ff7641acdfe374373e80'),
|
||||
(1, '226b3556a1804e38029fd237a66b1db0'),
|
||||
(1, 'b36725f672acde360df83bb877028d73'),
|
||||
(1, '6ba01a6240c18488040f9385de557081'),
|
||||
(1, '3b35edddcab0cac1aec5615dc86cd1f8'),
|
||||
(1, '8a9d0cf19bd5111f45b75ac99ef298e8'),
|
||||
(1, '1fa8e36ea62adfd1dbe8943a5cbd7457'),
|
||||
(1, '38304b8fd2f1552ee09b14be3c1d3f2e'),
|
||||
(1, 'acc5ce8a0c88cb4c5f74140e36f83e1f'),
|
||||
(1, '948fb19cc466d95cd9fc7188ad5411bf'),
|
||||
(1, 'c5024d345c3453b92b627bfc212dbf0e'),
|
||||
(1, 'ef5ca491b2c6298026a4eb425ec63c6b'),
|
||||
(1, '9b661821b671d44f7221a4a36ab80d8d');
|
||||
2144
backend/mock/songs.sql
Normal file
23
backend/src/db.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use diesel_async::{
|
||||
pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
|
||||
AsyncPgConnection,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
use crate::Opt;
|
||||
|
||||
pub type DbPool = Pool<AsyncPgConnection>;
|
||||
|
||||
pub async fn setup(opt: &Opt) -> eyre::Result<DbPool> {
|
||||
let manager = AsyncDieselConnectionManager::new(&opt.database_url);
|
||||
|
||||
info!("setting up database pool");
|
||||
let pool = Pool::builder(manager).build()?;
|
||||
|
||||
info!("testing database connection");
|
||||
let _db_test = pool.get().await?;
|
||||
|
||||
// TODO: migrations
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
178
backend/src/main.rs
Normal file
@ -0,0 +1,178 @@
|
||||
mod db;
|
||||
mod schema;
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{
|
||||
get,
|
||||
middleware::Logger,
|
||||
web::{self, Json},
|
||||
App, HttpRequest, HttpServer, Responder,
|
||||
};
|
||||
use clap::Parser;
|
||||
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use dotenv::dotenv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::DbPool;
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Queryable,
|
||||
Selectable,
|
||||
)]
|
||||
#[diesel(table_name = crate::schema::song)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Song {
|
||||
pub song_hash: String,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub cover: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub video: Option<String>,
|
||||
pub year: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub bpm: String,
|
||||
#[serde(rename = "duetsingerp1")]
|
||||
pub duet_singer_1: Option<String>,
|
||||
#[serde(rename = "duetsingerp2")]
|
||||
pub duet_singer_2: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Queryable, Selectable, Debug, Clone, Default)]
|
||||
#[diesel(table_name = crate::schema::custom_list)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct CustomList {
|
||||
id: i32,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn root() -> actix_web::Result<NamedFile> {
|
||||
let path: &Path = "dist/index.html".as_ref();
|
||||
Ok(NamedFile::open(path)?)
|
||||
}
|
||||
|
||||
async fn index(req: HttpRequest) -> actix_web::Result<NamedFile> {
|
||||
let path: PathBuf = req.match_info().query("filename").parse().unwrap();
|
||||
let path = Path::new("dist").join(path);
|
||||
Ok(NamedFile::open(path)?)
|
||||
}
|
||||
|
||||
#[get("/images/songs/{image}")]
|
||||
async fn song_image(
|
||||
path: web::Path<String>,
|
||||
opt: web::Data<Arc<Opt>>,
|
||||
) -> actix_web::Result<NamedFile> {
|
||||
let image = path.into_inner();
|
||||
let path = opt.covers_dir.join(image);
|
||||
Ok(NamedFile::open(path)?)
|
||||
}
|
||||
|
||||
#[get("/songs")]
|
||||
async fn songs(pool: web::Data<DbPool>) -> impl Responder {
|
||||
use schema::song::dsl::*;
|
||||
|
||||
let mut db = pool.get().await.unwrap();
|
||||
let songs = song.select(Song::as_select()).load(&mut db).await.unwrap();
|
||||
|
||||
Json(songs)
|
||||
}
|
||||
|
||||
#[get("/custom/lists")]
|
||||
async fn custom_lists(pool: web::Data<DbPool>) -> impl Responder {
|
||||
use schema::custom_list::dsl::*;
|
||||
|
||||
let mut db = pool.get().await.unwrap();
|
||||
let lists: Vec<String> = custom_list.select(name).load(&mut db).await.unwrap();
|
||||
|
||||
Json(lists)
|
||||
}
|
||||
|
||||
#[get("/custom/list/{list}")]
|
||||
async fn custom_list(pool: web::Data<DbPool>, path: web::Path<String>) -> impl Responder {
|
||||
use schema::custom_list::dsl::*;
|
||||
use schema::custom_list_entry::dsl::*;
|
||||
|
||||
let mut db = pool.get().await.unwrap();
|
||||
|
||||
let list: CustomList = custom_list
|
||||
.select(CustomList::as_select())
|
||||
.filter(name.eq(&*path))
|
||||
.get_result(&mut db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list_entries: Vec<String> = custom_list_entry
|
||||
.select(song_hash)
|
||||
.filter(list_id.eq(list.id))
|
||||
.load(&mut db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Json(list_entries)
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Opt {
|
||||
/// Address to bind to.
|
||||
#[clap(short, long, env = "BIND_ADDRESS", default_value = "0.0.0.0")]
|
||||
address: String,
|
||||
|
||||
/// Port to bind to.
|
||||
#[clap(short, long, env = "BIND_PORT", default_value = "8080")]
|
||||
port: u16,
|
||||
|
||||
/// Postgresql URL.
|
||||
#[clap(short, long, env = "DATABASE_URL")]
|
||||
database_url: String,
|
||||
|
||||
/// Directory where song covers are stored.
|
||||
#[clap(short, long, env = "COVERS_DIR")]
|
||||
covers_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
dotenv().ok();
|
||||
let opt = Arc::new(Opt::parse());
|
||||
env_logger::init();
|
||||
|
||||
let db_pool = db::setup(&opt).await?;
|
||||
let app = {
|
||||
let opt = Arc::clone(&opt);
|
||||
move || {
|
||||
let logger = Logger::default();
|
||||
|
||||
App::new()
|
||||
.wrap(logger)
|
||||
.app_data(web::Data::new(db_pool.clone()))
|
||||
.app_data(web::Data::new(Arc::clone(&opt)))
|
||||
.service(root)
|
||||
.service(songs)
|
||||
.service(song_image)
|
||||
.service(custom_list)
|
||||
.service(custom_lists)
|
||||
.route("/{filename:.*}", web::get().to(index))
|
||||
}
|
||||
};
|
||||
HttpServer::new(app)
|
||||
.bind((opt.address.as_str(), opt.port))?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
36
backend/src/schema.rs
Normal file
@ -0,0 +1,36 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
custom_list (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
custom_list_entry (list_id, song_hash) {
|
||||
list_id -> Int4,
|
||||
song_hash -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
song (song_hash) {
|
||||
song_hash -> Text,
|
||||
title -> Text,
|
||||
artist -> Text,
|
||||
cover -> Nullable<Text>,
|
||||
language -> Nullable<Text>,
|
||||
video -> Nullable<Text>,
|
||||
year -> Nullable<Text>,
|
||||
genre -> Nullable<Text>,
|
||||
bpm -> Text,
|
||||
duet_singer_1 -> Nullable<Text>,
|
||||
duet_singer_2 -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(custom_list_entry -> custom_list (list_id));
|
||||
diesel::joinable!(custom_list_entry -> song (song_hash));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(custom_list, custom_list_entry, song,);
|
||||
687
frontend/Cargo.lock
generated
Normal file
@ -0,0 +1,687 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "css_typegen"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/hulthe/css_typegen.git?branch=master#ed1c4b1b7c8bf19e1ce045cafa12f3886041134c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enclose"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-console"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
|
||||
dependencies = [
|
||||
"gloo-utils 0.2.0",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-events"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-file"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gloo-events",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-net"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"gloo-utils 0.2.0",
|
||||
"http",
|
||||
"js-sys",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "seed"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c0e296ea0569d20467e9a1df3cb6ed66ce3b791a7eaf1e1110ae231f75e2b46"
|
||||
dependencies = [
|
||||
"enclose",
|
||||
"futures",
|
||||
"getrandom",
|
||||
"gloo-file",
|
||||
"gloo-timers",
|
||||
"gloo-utils 0.1.7",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"rand",
|
||||
"uuid",
|
||||
"version_check",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.188"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.188"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "singit2"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"css_typegen",
|
||||
"csv",
|
||||
"gloo-console",
|
||||
"gloo-net",
|
||||
"rand",
|
||||
"seed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
20
frontend/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "singit_web"
|
||||
version = "1.0.0"
|
||||
authors = ["Joakim Hulthe <joakim@hulthe.net"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
seed = "0.10.0"
|
||||
serde = { version = "1.0.0", features = ["derive"] }
|
||||
serde_json = "1.0.0"
|
||||
rand = "0.8.5"
|
||||
gloo-console = "0.3.0"
|
||||
gloo-net = "0.4.0"
|
||||
csv = "1.2.2"
|
||||
thiserror = "1.0.48"
|
||||
wasm-bindgen = "0.2.87"
|
||||
|
||||
[dependencies.css_typegen]
|
||||
git = "https://github.com/hulthe/css_typegen.git"
|
||||
branch = "master"
|
||||
26
frontend/build.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::io;
|
||||
|
||||
fn main() {
|
||||
index_default_covers().expect("index default covers");
|
||||
}
|
||||
|
||||
/// Index all pngs in static/images/default_covers and expose the list in the build as an
|
||||
/// environment variable DEFAULT_SONG_COVERS. This list includes the path to the images so the
|
||||
/// frontend can fetch them.
|
||||
fn index_default_covers() -> io::Result<()> {
|
||||
let mut files = vec![];
|
||||
for dir in fs::read_dir("static/images/default_covers")? {
|
||||
let path = dir?.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("png") {
|
||||
let path = Path::new("/").join(path.strip_prefix("static").unwrap());
|
||||
files.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let list = files.join(",");
|
||||
println!("cargo:rustc-env=DEFAULT_SONG_COVERS={list}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
403
frontend/src/app.rs
Normal file
@ -0,0 +1,403 @@
|
||||
use crate::css::C;
|
||||
use crate::custom_list::{fetch_custom_song_list, fetch_custom_song_list_index, CustomLists};
|
||||
use crate::fetch::fetch_list_of;
|
||||
use crate::fuzzy::FuzzyScore;
|
||||
use crate::query::ParsedQuery;
|
||||
use crate::song::Song;
|
||||
use gloo_console::error;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use seed::app::cmds::timeout;
|
||||
use seed::browser::util::document;
|
||||
use seed::prelude::*;
|
||||
use seed::{attrs, button, div, empty, img, input, p, span, C, IF};
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use web_sys::Element;
|
||||
|
||||
pub struct Model {
|
||||
songs: Vec<(Reverse<FuzzyScore>, Song)>,
|
||||
|
||||
/// Custom song lists, lazily loaded.
|
||||
custom_lists: CustomLists,
|
||||
|
||||
/// The search string.
|
||||
query: String,
|
||||
|
||||
/// The number of songs currently in the dom. Goes up when the user scrolls down.
|
||||
shown_songs: usize,
|
||||
|
||||
/// The number of songs that didn't match the search critera.
|
||||
hidden_songs: usize,
|
||||
|
||||
/// Whether we're filtering by video.
|
||||
filter_video: bool,
|
||||
|
||||
/// Whether we're filtering by duets.
|
||||
filter_duets: bool,
|
||||
|
||||
query_placeholder: String,
|
||||
query_placeholder_len: usize,
|
||||
|
||||
autotyper: Option<CmdHandle>,
|
||||
|
||||
/// URLs of some defaults for songs with missing cover art.
|
||||
default_song_covers: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum Loading<T> {
|
||||
/// The resource has not started loading.
|
||||
#[default]
|
||||
NotLoaded,
|
||||
|
||||
/// The resource is currently loading.
|
||||
InProgress,
|
||||
|
||||
/// The resource has loaded.
|
||||
Loaded(T),
|
||||
}
|
||||
|
||||
const SCROLL_THRESHOLD: usize = 50;
|
||||
const INITIAL_ELEM_COUNT: usize = 100;
|
||||
|
||||
pub enum Msg {
|
||||
/// Loaded songs.
|
||||
Songs(Vec<Song>),
|
||||
|
||||
/// Loaded custom song index.
|
||||
CustomSongLists(Vec<String>),
|
||||
|
||||
/// Loaded custom song list.
|
||||
CustomSongList {
|
||||
list: String,
|
||||
song_hashes: HashSet<String>,
|
||||
},
|
||||
|
||||
/// The user entered something into the search field
|
||||
Search(String),
|
||||
|
||||
/// The user pressed the Toggle Video button
|
||||
ToggleVideo,
|
||||
|
||||
/// The user pressed the Toggle Duets button
|
||||
ToggleDuets,
|
||||
|
||||
/// The user pressed the Shuffle button
|
||||
Shuffle,
|
||||
|
||||
/// The user scrolled the song list
|
||||
Scroll,
|
||||
|
||||
/// Type stuff in the search input placeholder
|
||||
Autotyper,
|
||||
}
|
||||
|
||||
pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
orders.perform_cmd(fetch_songs());
|
||||
orders.perform_cmd(fetch_custom_song_list_index());
|
||||
|
||||
// get list of default song covers. see build.rs
|
||||
const DEFAULT_SONG_COVERS: &str = env!("DEFAULT_SONG_COVERS");
|
||||
let default_song_covers = DEFAULT_SONG_COVERS.split(',').collect();
|
||||
|
||||
Model {
|
||||
songs: vec![],
|
||||
custom_lists: Default::default(),
|
||||
query: String::new(),
|
||||
hidden_songs: 0,
|
||||
shown_songs: INITIAL_ELEM_COUNT,
|
||||
filter_video: false,
|
||||
filter_duets: false,
|
||||
query_placeholder: String::from("Sök"),
|
||||
query_placeholder_len: 0,
|
||||
autotyper: Some(orders.perform_cmd_with_handle(timeout(500, || Msg::Autotyper))),
|
||||
default_song_covers,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_song_list(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
|
||||
if model.query.is_empty() {
|
||||
model.filter_duets = false;
|
||||
model.filter_video = false;
|
||||
update(Msg::Shuffle, model, orders);
|
||||
} else {
|
||||
let query = ParsedQuery::parse(&model.query);
|
||||
model.filter_duets = query.duet == Some(true);
|
||||
model.filter_video = query.video == Some(true);
|
||||
|
||||
if let Some(name) = query.list {
|
||||
if let Some(l @ Loading::NotLoaded) = model.custom_lists.get_mut(name) {
|
||||
orders.perform_cmd(fetch_custom_song_list(name.to_string()));
|
||||
*l = Loading::InProgress;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate search scores & sort list
|
||||
for (score, song) in model.songs.iter_mut() {
|
||||
let new_score = song.fuzzy_compare(&query, &model.custom_lists);
|
||||
if new_score < Default::default() {
|
||||
model.hidden_songs += 1;
|
||||
}
|
||||
|
||||
*score = Reverse(new_score);
|
||||
}
|
||||
model.songs.sort_unstable();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Songs(songs) => {
|
||||
model.songs = songs
|
||||
.into_iter()
|
||||
.map(|song| (Default::default(), song))
|
||||
.collect();
|
||||
}
|
||||
Msg::CustomSongLists(lists) => {
|
||||
model.custom_lists = lists
|
||||
.into_iter()
|
||||
.map(|list| (list, Loading::NotLoaded))
|
||||
.collect();
|
||||
}
|
||||
Msg::CustomSongList { list, song_hashes } => {
|
||||
let query = ParsedQuery::parse(&model.query);
|
||||
let update_list = query.list == Some(&list);
|
||||
|
||||
*model.custom_lists.entry(list).or_default() = Loading::Loaded(song_hashes);
|
||||
|
||||
if update_list {
|
||||
update_song_list(model, orders);
|
||||
}
|
||||
}
|
||||
Msg::Search(query) => {
|
||||
model.query = query;
|
||||
update_song_list(model, orders);
|
||||
}
|
||||
Msg::ToggleVideo => {
|
||||
let mut query = ParsedQuery::parse(&model.query);
|
||||
query.video = match query.video {
|
||||
Some(true) => None,
|
||||
None | Some(false) => Some(true),
|
||||
};
|
||||
update(Msg::Search(query.to_string()), model, orders);
|
||||
}
|
||||
Msg::ToggleDuets => {
|
||||
let mut query = ParsedQuery::parse(&model.query);
|
||||
query.duet = match query.duet {
|
||||
Some(true) => None,
|
||||
None | Some(false) => Some(true),
|
||||
};
|
||||
update(Msg::Search(query.to_string()), model, orders);
|
||||
}
|
||||
Msg::Shuffle => {
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
model.query.clear();
|
||||
model.songs.shuffle(&mut thread_rng());
|
||||
autotype_song(model, orders);
|
||||
}
|
||||
Msg::Scroll => {
|
||||
let Some((scroll, max_scroll)) = get_scroll() else {
|
||||
error!("Failed to get song list element by id:", SONG_LIST_ID);
|
||||
return;
|
||||
};
|
||||
|
||||
let scroll_left: i32 = max_scroll - scroll;
|
||||
|
||||
// when there are fewer elements than this below the scroll viewport, add more
|
||||
const ELEMENT_HEIGHT: i32 = 48;
|
||||
|
||||
if scroll_left < ELEMENT_HEIGHT * SCROLL_THRESHOLD as i32 {
|
||||
model.shown_songs += 1;
|
||||
orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll));
|
||||
}
|
||||
}
|
||||
Msg::Autotyper => {
|
||||
model.query_placeholder_len += 1;
|
||||
while !model
|
||||
.query_placeholder
|
||||
.is_char_boundary(model.query_placeholder_len)
|
||||
{
|
||||
model.query_placeholder_len += 1;
|
||||
}
|
||||
|
||||
if model.query_placeholder_len < model.query_placeholder.len() {
|
||||
model.autotyper =
|
||||
Some(orders.perform_cmd_with_handle(timeout(80, || Msg::Autotyper)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
|
||||
let song_card = |song: &Song| -> Node<Msg> {
|
||||
div![
|
||||
C![C.song_item],
|
||||
img![
|
||||
C![C.song_item_cover],
|
||||
match song.cover {
|
||||
Some(_) => attrs! {At::Src => format!("/images/songs/{}.png", song.song_hash)},
|
||||
None => {
|
||||
// use a DefaultHasher to turn the song_hash string into a number we can
|
||||
// use to give the song a psuedo-random default cover.
|
||||
let mut hasher = DefaultHasher::new();
|
||||
song.song_hash.hash(&mut hasher);
|
||||
let hash = hasher.finish() as usize;
|
||||
let cover_i = hash % model.default_song_covers.len();
|
||||
let cover = model.default_song_covers[cover_i];
|
||||
attrs! { At::Src => cover }
|
||||
}
|
||||
},
|
||||
],
|
||||
div![
|
||||
C![C.song_item_info],
|
||||
div![C![C.song_item_title], &song.title],
|
||||
div![
|
||||
C![C.song_item_artist],
|
||||
span![&song.artist],
|
||||
if let Some(year) = song.year.as_ref() {
|
||||
span![" (", year, ")"]
|
||||
} else {
|
||||
empty![]
|
||||
}
|
||||
],
|
||||
],
|
||||
div![
|
||||
C![C.song_gizmos],
|
||||
match &song.genre {
|
||||
Some(genre) => div![
|
||||
C![C.gizmo, C.note_icon, C.tooltip],
|
||||
span![C![C.tooltiptext], genre],
|
||||
],
|
||||
None => empty![],
|
||||
},
|
||||
match &song.language {
|
||||
Some(language) => div![
|
||||
C![C.gizmo, C.flag_icon, C.tooltip],
|
||||
span![C![C.tooltiptext], language],
|
||||
],
|
||||
None => empty![],
|
||||
},
|
||||
IF![song.video.is_some() => div![
|
||||
C![C.gizmo, C.video_icon, C.tooltip],
|
||||
span![
|
||||
C![C.tooltiptext],
|
||||
"Musikvideo",
|
||||
],
|
||||
]],
|
||||
match (&song.duet_singer_1, &song.duet_singer_2) {
|
||||
(Some(p1), Some(p2)) => div![
|
||||
C![C.gizmo, C.duet_icon, C.tooltip],
|
||||
span![
|
||||
C![C.tooltiptext],
|
||||
"Duet",
|
||||
div![
|
||||
C![C.marquee],
|
||||
// add duplicates to get the repeating marquee effect
|
||||
p![" 🗲 ", p1, " 🗲 ", p2, " 🗲 ", p1, " 🗲 ", p2]
|
||||
],
|
||||
],
|
||||
],
|
||||
_ => empty![],
|
||||
},
|
||||
],
|
||||
]
|
||||
};
|
||||
|
||||
vec![
|
||||
div![
|
||||
C![C.song_search_bar],
|
||||
input![
|
||||
C![C.song_search_field],
|
||||
input_ev(Ev::Input, Msg::Search),
|
||||
attrs! {
|
||||
At::Placeholder => &model.query_placeholder[..model.query_placeholder_len],
|
||||
At::Value => model.query,
|
||||
},
|
||||
],
|
||||
button![
|
||||
C![C.song_sort_button, C.tooltip],
|
||||
IF![model.filter_duets => C![C.song_sort_button_selected]],
|
||||
ev(Ev::Click, |_| Msg::ToggleDuets),
|
||||
span![C![C.tooltiptext], "Endast Duetter"],
|
||||
"D",
|
||||
],
|
||||
button![
|
||||
C![C.song_sort_button, C.tooltip],
|
||||
IF![model.filter_video => C![C.song_sort_button_selected]],
|
||||
ev(Ev::Click, |_| Msg::ToggleVideo),
|
||||
span![C![C.tooltiptext], "Endast med Video"],
|
||||
"V",
|
||||
],
|
||||
button![
|
||||
C![C.song_sort_button, C.song_sort_button_right, C.tooltip],
|
||||
IF![model.filter_video => C![C.song_sort_button_selected]],
|
||||
ev(Ev::Click, |_| Msg::Shuffle),
|
||||
span![C![C.tooltiptext], "Blanda låtar"],
|
||||
"🔀",
|
||||
],
|
||||
],
|
||||
div![
|
||||
C![C.song_list],
|
||||
attrs! {At::Id => SONG_LIST_ID},
|
||||
ev(Ev::Scroll, |_| Msg::Scroll),
|
||||
model
|
||||
.songs
|
||||
.iter()
|
||||
.map(|(_, song)| song)
|
||||
.map(song_card)
|
||||
.take(model.songs.len() - model.hidden_songs)
|
||||
.take(model.shown_songs),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
async fn fetch_songs() -> Option<Msg> {
|
||||
let mut songs: Vec<Song> = match fetch_list_of("/songs").await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
error!("Error fetching songs:", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
songs.shuffle(&mut thread_rng());
|
||||
|
||||
Some(Msg::Songs(songs))
|
||||
}
|
||||
|
||||
pub fn autotype_song(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
let (_, song) = &model.songs[0];
|
||||
model.query_placeholder = ParsedQuery::random(song, &mut thread_rng()).to_string();
|
||||
model.query_placeholder_len = 0;
|
||||
model.autotyper = Some(orders.perform_cmd_with_handle(timeout(100, || Msg::Autotyper)));
|
||||
}
|
||||
|
||||
const SONG_LIST_ID: &str = "song_list";
|
||||
|
||||
fn get_song_list_element() -> Option<Element> {
|
||||
document().get_element_by_id(SONG_LIST_ID)
|
||||
}
|
||||
|
||||
fn scroll_to_top() {
|
||||
if let Some(elem) = get_song_list_element() {
|
||||
elem.scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scroll() -> Option<(i32, i32)> {
|
||||
let list = get_song_list_element()?;
|
||||
let scroll = list.scroll_top();
|
||||
let height = list.client_height();
|
||||
let max = (list.scroll_height() - height).max(0);
|
||||
Some((scroll, max))
|
||||
}
|
||||
@ -4,4 +4,4 @@ use css_typegen::css_typegen;
|
||||
|
||||
// Generate rust types for css-classes.
|
||||
// Used for autocompletion and extra compile-time checks.
|
||||
css_typegen!("static/styles");
|
||||
css_typegen!("frontend/static/styles");
|
||||
34
frontend/src/custom_list.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use gloo_console::error;
|
||||
|
||||
use crate::{
|
||||
app::{Loading, Msg},
|
||||
fetch::fetch_list_of,
|
||||
};
|
||||
|
||||
pub type CustomLists = HashMap<String, Loading<HashSet<String>>>;
|
||||
|
||||
pub async fn fetch_custom_song_list_index() -> Option<Msg> {
|
||||
let custom_lists: Vec<String> = match fetch_list_of("/custom/lists").await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
error!("Failed fetching custom song list index:", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Msg::CustomSongLists(custom_lists))
|
||||
}
|
||||
|
||||
pub async fn fetch_custom_song_list(list: String) -> Option<Msg> {
|
||||
let song_hashes: HashSet<String> = match fetch_list_of(format!("/custom/list/{list}")).await {
|
||||
Ok(response) => response.into_iter().collect(),
|
||||
Err(e) => {
|
||||
error!("Failed fetching custom song list:", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Msg::CustomSongList { list, song_hashes })
|
||||
}
|
||||
70
frontend/src/fetch.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use gloo_net::http::{Request, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use wasm_bindgen::{JsError, JsValue};
|
||||
|
||||
const HTTP_ACCEPT: &str = concat!("text/csv, ", "application/json;q=0.9");
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FetchError {
|
||||
/// The request returned a non-2XX status code.
|
||||
#[error("server responded with {code} {text}")]
|
||||
Status { code: u16, text: String },
|
||||
|
||||
/// The response contained an unrecognized or missing content type.
|
||||
#[error("unknown content type {0:?}")]
|
||||
UnknownContentType(Option<String>),
|
||||
|
||||
/// Another error occured.
|
||||
#[error("{0}")]
|
||||
Other(#[from] gloo_net::Error),
|
||||
|
||||
#[error("error deserializing csv: {0}")]
|
||||
Csv(#[from] csv::Error),
|
||||
}
|
||||
|
||||
impl From<FetchError> for JsValue {
|
||||
fn from(e: FetchError) -> Self {
|
||||
JsError::new(&e.to_string()).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a GET request.
|
||||
pub async fn fetch(url: impl AsRef<str>) -> Result<Response, FetchError> {
|
||||
let response = Request::get(url.as_ref())
|
||||
.header("accept", HTTP_ACCEPT)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.ok() {
|
||||
return Err(FetchError::Status {
|
||||
code: response.status(),
|
||||
text: response.status_text(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Perform a GET request and try to deserialize the response as a `Vec<T>`.
|
||||
pub async fn fetch_list_of<T: DeserializeOwned>(
|
||||
url: impl AsRef<str>,
|
||||
) -> Result<Vec<T>, FetchError> {
|
||||
let response = fetch(url.as_ref()).await?;
|
||||
let headers = response.headers();
|
||||
let content_type = headers.get("Content-Type").map(|s| s.to_lowercase());
|
||||
|
||||
match content_type.as_deref() {
|
||||
Some("text/csv") => {
|
||||
let text = response.text().await?;
|
||||
let reader = csv::Reader::from_reader(Cursor::new(text)).into_deserialize();
|
||||
let list = reader
|
||||
.map(|r| r.map_err(FetchError::from))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(list)
|
||||
}
|
||||
Some("application/json") => Ok(response.json().await?),
|
||||
_ => Err(FetchError::UnknownContentType(content_type)),
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
mod app;
|
||||
mod css;
|
||||
mod custom_list;
|
||||
mod fetch;
|
||||
mod fuzzy;
|
||||
mod query;
|
||||
mod song;
|
||||
|
||||
use seed::prelude::wasm_bindgen;
|
||||
use seed::App;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
fn main() {
|
||||
App::start("app", app::init, app::update, app::view);
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
use crate::song::Song;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::Not;
|
||||
@ -8,10 +11,10 @@ pub struct ParsedQuery<'a> {
|
||||
pub plain: Option<Cow<'a, str>>,
|
||||
|
||||
/// Query a specific title
|
||||
pub title: Option<&'a str>,
|
||||
pub title: Option<Cow<'a, str>>,
|
||||
|
||||
/// Query a specific artist
|
||||
pub artist: Option<&'a str>,
|
||||
pub artist: Option<Cow<'a, str>>,
|
||||
|
||||
/// Whether the song is a duet
|
||||
pub duet: Option<bool>,
|
||||
@ -27,6 +30,9 @@ pub struct ParsedQuery<'a> {
|
||||
|
||||
/// Query from a specifc year
|
||||
pub year: Option<&'a str>,
|
||||
|
||||
/// Query songs from the specified custom list.
|
||||
pub list: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> ParsedQuery<'a> {
|
||||
@ -40,25 +46,84 @@ impl<'a> ParsedQuery<'a> {
|
||||
|
||||
for (k, v) in kvs {
|
||||
match k {
|
||||
"title" => parsed.title = Some(v),
|
||||
"artist" => parsed.artist = Some(v),
|
||||
"title" => parsed.title = Some(Cow::Borrowed(v)),
|
||||
"artist" => parsed.artist = Some(Cow::Borrowed(v)),
|
||||
"duet" => parsed.duet = parse_bool(v),
|
||||
"video" => parsed.video = parse_bool(v),
|
||||
"lang" => parsed.language = Some(v),
|
||||
"genre" => parsed.genre = Some(v),
|
||||
"year" => parsed.year = Some(v),
|
||||
"list" => parsed.list = Some(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
parsed
|
||||
}
|
||||
|
||||
/// Generate a parsed query with a few random fields matching a song
|
||||
pub fn random<R: Rng>(song: &'a Song, rng: &mut R) -> Self {
|
||||
let until_space = |s: &'a str| -> &'a str { s.split_whitespace().next().unwrap_or("") };
|
||||
|
||||
let join_spaces = |s: &'a str| -> Cow<'a, str> {
|
||||
let s = s.trim();
|
||||
if s.contains(char::is_whitespace) {
|
||||
s.replace(char::is_whitespace, "").into()
|
||||
} else {
|
||||
Cow::Borrowed(s)
|
||||
}
|
||||
};
|
||||
|
||||
let mut primary_fields: [&dyn Fn(Self) -> Self; 4] = [
|
||||
&|query| Self {
|
||||
plain: Some(Cow::Borrowed(&song.title)),
|
||||
..query
|
||||
},
|
||||
&|query| Self {
|
||||
plain: Some(Cow::Borrowed(&song.artist)),
|
||||
..query
|
||||
},
|
||||
&|query| Self {
|
||||
title: Some(join_spaces(&song.title)),
|
||||
..query
|
||||
},
|
||||
&|query| Self {
|
||||
artist: Some(join_spaces(&song.artist)),
|
||||
..query
|
||||
},
|
||||
];
|
||||
|
||||
let mut extra_fields: [&dyn Fn(Self) -> Self; 3] = [
|
||||
&|query| Self {
|
||||
language: song.language.as_deref().map(until_space),
|
||||
..query
|
||||
},
|
||||
&|query| Self {
|
||||
genre: song.genre.as_deref().map(until_space),
|
||||
..query
|
||||
},
|
||||
&|query| Self {
|
||||
year: song.year.as_deref().map(until_space),
|
||||
..query
|
||||
},
|
||||
];
|
||||
|
||||
primary_fields.shuffle(rng);
|
||||
extra_fields.shuffle(rng);
|
||||
|
||||
let primary_fields = primary_fields.into_iter().take(1);
|
||||
let extra_fields = extra_fields.into_iter().take(rng.gen_range(0..2));
|
||||
|
||||
primary_fields
|
||||
.chain(extra_fields)
|
||||
.fold(Self::default(), |query, field| field(query))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool(s: &str) -> Option<bool> {
|
||||
match s {
|
||||
"true" | "yes" => Some(true),
|
||||
"false" | "no" => Some(false),
|
||||
"true" | "yes" | "y" => Some(true),
|
||||
"false" | "no" | "n" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -75,7 +140,7 @@ fn extract_plain(s: &str) -> Option<Cow<str>> {
|
||||
a
|
||||
});
|
||||
|
||||
plain.is_empty().not().then(|| Cow::Owned(plain))
|
||||
plain.is_empty().not().then_some(Cow::Owned(plain))
|
||||
}
|
||||
|
||||
fn extract_key_values(s: &str) -> impl Iterator<Item = (&str, &str)> {
|
||||
@ -111,6 +176,7 @@ impl Display for ParsedQuery<'_> {
|
||||
w("lang:", display(&self.language))?;
|
||||
w("genre:", display(&self.genre))?;
|
||||
w("year:", display(&self.year))?;
|
||||
w("list:", display(&self.list))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,14 +1,16 @@
|
||||
use crate::app::Loading;
|
||||
use crate::custom_list::CustomLists;
|
||||
use crate::fuzzy::{self, FuzzyScore};
|
||||
use crate::query::ParsedQuery;
|
||||
use serde::Deserialize;
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Song {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub cover: Option<String>,
|
||||
pub song_hash: String,
|
||||
pub cover: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub video: Option<String>,
|
||||
pub year: Option<String>,
|
||||
@ -27,22 +29,21 @@ impl Song {
|
||||
.zip(self.duet_singer_2.as_deref())
|
||||
}
|
||||
|
||||
pub fn fuzzy_compare(&self, query: &ParsedQuery) -> FuzzyScore {
|
||||
let bad = || -1;
|
||||
pub fn fuzzy_compare(&self, query: &ParsedQuery, custom_lists: &CustomLists) -> FuzzyScore {
|
||||
let bad: FuzzyScore = -1;
|
||||
|
||||
let filter_strs = |query: Option<&str>, item: Option<&str>| {
|
||||
if let Some(query) = query {
|
||||
match item {
|
||||
Some(item) => {
|
||||
let score = fuzzy::compare(item.chars(), query.chars());
|
||||
if score < fuzzy::max_score(query) / 2 {
|
||||
return false;
|
||||
}
|
||||
score == fuzzy::max_score(query)
|
||||
}
|
||||
None => return false,
|
||||
None => false,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
true
|
||||
};
|
||||
|
||||
let filter_bool =
|
||||
@ -57,7 +58,17 @@ impl Song {
|
||||
];
|
||||
|
||||
if !filters.iter().all(|f| f()) {
|
||||
return bad();
|
||||
return bad;
|
||||
}
|
||||
|
||||
if let Some(list) = query.list {
|
||||
let Some(Loading::Loaded(list)) = custom_lists.get(list) else {
|
||||
return bad;
|
||||
};
|
||||
|
||||
if !list.contains(&self.song_hash) {
|
||||
return bad;
|
||||
}
|
||||
}
|
||||
|
||||
let mut score = FuzzyScore::default();
|
||||
@ -67,12 +78,12 @@ impl Song {
|
||||
score = max(title_score, artist_score);
|
||||
}
|
||||
|
||||
if let Some(title) = query.title {
|
||||
if let Some(title) = &query.title {
|
||||
let new_score = fuzzy::compare(self.title.chars(), title.chars());
|
||||
score = max(score, new_score);
|
||||
}
|
||||
|
||||
if let Some(artist) = query.artist {
|
||||
if let Some(artist) = &query.artist {
|
||||
let new_score = fuzzy::compare(self.artist.chars(), artist.chars());
|
||||
score = max(score, new_score);
|
||||
}
|
||||
80
frontend/static/images/background.svg
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="151.118px" height="151.118px" viewBox="0 0 151.118 151.118" enable-background="new 0 0 151.118 151.118" xml:space="preserve">
|
||||
<circle fill="#FFFFFF" stroke="#000000" stroke-width="3" cx="75.559" cy="75.56" r="71.208"/>
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#00B3E6" stroke="#000000" d="M79.393,18.702c-1.27-0.085-2.543-0.143-3.833-0.143c-6.136,0-12.04,0.98-17.578,2.774 c0.458,1.249,0.721,2.592,0.721,4c0,6.429-5.211,11.64-11.64,11.64c-3.645,0-6.895-1.678-9.029-4.3 c-3.313,2.902-6.285,6.18-8.853,9.771h50.212V18.702z M103.104,25.654v16.79h18.835C117,35.541,110.568,29.783,103.104,25.654z M125.401,47.905h-22.298v46.557c0,0-1.151,11.496,11.495,11.496h9.169c5.562-8.801,8.792-19.22,8.792-30.397 C132.56,65.522,129.956,56.097,125.401,47.905z M79.393,94.461c0,0,0-23.276,0-34.917s-11.639-11.64-11.639-11.64h-7.759v46.557 c0,11.641,11.639,11.641,11.639,11.641v7.688H33.283c10.43,11.526,25.507,18.771,42.276,18.771 c16.77,0,31.846-7.244,42.275-18.771h-3.236C114.599,113.788,79.393,115.814,79.393,94.461z M36.717,59.545 c0-10.017-8.614-11.412-11.018-11.606c-4.544,8.184-7.14,17.597-7.14,27.621c0,11.17,3.226,21.58,8.777,30.376 c9.374-1.402,9.38-11.475,9.38-11.475S36.717,71.185,36.717,59.545z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#00B3E6" stroke="#000000" stroke-width="3" d="M79.393,18.702c-1.27-0.085-2.543-0.143-3.833-0.143 c-6.136,0-12.04,0.98-17.578,2.774c0.458,1.249,0.721,2.592,0.721,4c0,6.429-5.211,11.64-11.64,11.64 c-3.645,0-6.895-1.678-9.029-4.3c-3.313,2.902-6.285,6.18-8.853,9.771h50.212V18.702z M103.104,25.654v16.79h18.835 C117,35.541,110.568,29.783,103.104,25.654z M125.401,47.905h-22.298v46.557c0,0-1.151,11.496,11.495,11.496h9.169 c5.562-8.801,8.792-19.22,8.792-30.397C132.56,65.522,129.956,56.097,125.401,47.905z M79.393,94.461c0,0,0-23.276,0-34.917 s-11.639-11.64-11.639-11.64h-7.759v46.557c0,11.641,11.639,11.641,11.639,11.641v7.688H33.283 c10.43,11.526,25.507,18.771,42.276,18.771c16.77,0,31.846-7.244,42.275-18.771h-3.236 C114.599,113.788,79.393,115.814,79.393,94.461z M36.717,59.545c0-10.017-8.614-11.412-11.018-11.606 c-4.544,8.184-7.14,17.597-7.14,27.621c0,11.17,3.226,21.58,8.777,30.376c9.374-1.402,9.38-11.475,9.38-11.475 S36.717,71.185,36.717,59.545z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M8.724,80.924l1.055-0.127l0.217,1.832l5.027-0.598l0.151,1.272l-5.027,0.597l0.217,1.831L9.31,85.857L8.724,80.924z"/>
|
||||
<path d="M9.61,74.781l0.074,3.155l1.296-0.029l-0.067-2.891l1.062-0.024l0.067,2.889l1.578-0.036l-0.077-3.312l1.093-0.025 l0.107,4.562l-6.124,0.144l-0.103-4.405L9.61,74.781z"/>
|
||||
<path d="M8.573,72.581l0.099-1.246l2.519,0.201L8.86,68.973l0.131-1.645l2.312,2.705l3.804-2.348l-0.13,1.637l-2.817,1.67 l0.576,0.666l2.042,0.163l-0.099,1.246L8.573,72.581z"/>
|
||||
<path d="M9.257,65.512l0.247-1.311l4.612-1.558l-4.17-0.786l0.22-1.167l6.019,1.134l-0.235,1.251l-4.699,1.603l4.245,0.8 l-0.22,1.167L9.257,65.512z"/>
|
||||
<path d="M17.4,55.628c0.32,0.581,0.351,1.289,0.095,2.124c-0.256,0.835-0.679,1.404-1.269,1.706 c-0.739,0.421-1.633,0.471-2.683,0.149c-1.071-0.328-1.783-0.87-2.139-1.626c-0.319-0.581-0.351-1.289-0.095-2.124 s0.679-1.404,1.269-1.706c0.717-0.425,1.611-0.474,2.682-0.146C16.31,54.327,17.023,54.868,17.4,55.628z M16.27,56.118 c-0.268-0.392-0.729-0.688-1.384-0.888c-0.652-0.2-1.2-0.213-1.643-0.039c-0.443,0.173-0.738,0.501-0.885,0.982 s-0.087,0.92,0.179,1.315c0.267,0.395,0.728,0.693,1.383,0.894c0.654,0.2,1.203,0.212,1.646,0.034 c0.442-0.178,0.738-0.507,0.885-0.989C16.598,56.946,16.538,56.51,16.27,56.118z"/>
|
||||
<path d="M13.063,51.3l0.5-1.18l4.633,1.962l1.182-2.791l1.007,0.426l-1.682,3.971L13.063,51.3z"/>
|
||||
<path d="M22.694,44.202c0.194,0.634,0.081,1.333-0.341,2.099c-0.421,0.765-0.951,1.236-1.59,1.411 c-0.81,0.262-1.695,0.128-2.657-0.401c-0.981-0.54-1.568-1.216-1.761-2.029c-0.194-0.633-0.081-1.333,0.34-2.098 c0.421-0.766,0.951-1.236,1.59-1.412c0.789-0.27,1.673-0.134,2.654,0.405C21.893,42.706,22.48,43.381,22.694,44.202z M21.487,44.451c-0.183-0.438-0.574-0.822-1.174-1.152c-0.598-0.329-1.131-0.453-1.6-0.374c-0.469,0.08-0.825,0.34-1.067,0.781 c-0.243,0.441-0.273,0.882-0.093,1.324c0.181,0.442,0.571,0.828,1.171,1.158s1.135,0.453,1.604,0.369 c0.469-0.084,0.826-0.346,1.068-0.787C21.64,45.329,21.67,44.889,21.487,44.451z"/>
|
||||
<path d="M23.325,36.713c-0.397-0.163-0.773-0.14-1.126,0.071c-0.196,0.119-0.368,0.282-0.517,0.492 c-0.284,0.401-0.366,0.838-0.247,1.31c0.119,0.472,0.482,0.922,1.089,1.352c0.611,0.433,1.142,0.599,1.594,0.5 c0.452-0.099,0.803-0.328,1.056-0.684c0.247-0.349,0.35-0.706,0.308-1.07c-0.042-0.364-0.206-0.687-0.493-0.968l-0.824,1.164 l-0.829-0.587l1.472-2.079l2.678,1.896l-0.488,0.688l-0.692-0.347c0.094,0.369,0.132,0.667,0.116,0.896 c-0.027,0.395-0.182,0.793-0.466,1.195c-0.469,0.662-1.081,1.042-1.837,1.141c-0.771,0.115-1.556-0.11-2.353-0.675 c-0.807-0.571-1.296-1.251-1.468-2.04c-0.172-0.789,0-1.548,0.518-2.279c0.448-0.633,0.969-1.028,1.562-1.186 c0.593-0.158,1.15-0.094,1.669,0.189L23.325,36.713z"/>
|
||||
<path d="M27.742,34.873c0.234,0.155,0.441,0.236,0.62,0.241c0.326,0.01,0.656-0.175,0.992-0.554 c0.201-0.227,0.339-0.434,0.415-0.619c0.143-0.353,0.085-0.643-0.172-0.871c-0.151-0.133-0.326-0.17-0.525-0.111 c-0.197,0.061-0.453,0.208-0.768,0.439l-0.541,0.394c-0.532,0.386-0.943,0.616-1.233,0.69c-0.489,0.128-0.957-0.006-1.406-0.402 c-0.409-0.361-0.616-0.813-0.62-1.354s0.251-1.103,0.768-1.688c0.431-0.488,0.927-0.791,1.487-0.909 c0.561-0.118,1.092,0.029,1.594,0.443l-0.817,0.925c-0.291-0.227-0.594-0.277-0.911-0.152c-0.211,0.084-0.416,0.238-0.615,0.463 c-0.221,0.251-0.348,0.495-0.381,0.732c-0.033,0.237,0.04,0.435,0.218,0.591c0.164,0.145,0.351,0.179,0.562,0.103 c0.137-0.047,0.378-0.193,0.723-0.438l0.894-0.632c0.392-0.277,0.734-0.442,1.028-0.496c0.456-0.083,0.888,0.056,1.295,0.416 c0.418,0.369,0.623,0.837,0.614,1.403c-0.009,0.566-0.273,1.143-0.792,1.731c-0.531,0.601-1.085,0.953-1.662,1.056 c-0.577,0.103-1.104-0.056-1.581-0.478L27.742,34.873z"/>
|
||||
<path d="M32.433,25.651l-2.314,2.146L31,28.748l2.12-1.965l0.722,0.779l-2.12,1.965l1.073,1.157l2.429-2.252l0.744,0.802 l-3.346,3.102l-4.164-4.492l3.231-2.996L32.433,25.651z"/>
|
||||
<path d="M33.397,23.413l1-0.75l1.518,2.021l0.377-3.443l1.319-0.991l-0.497,3.523l4.278,1.297l-1.313,0.986l-3.117-1.006 l-0.118,0.873l1.23,1.638l-1,0.75L33.397,23.413z"/>
|
||||
<path d="M42.951,16.963l0.55,0.909l-1.577,0.956l2.623,4.33l-1.096,0.664l-2.623-4.33l-1.577,0.956L38.7,19.539L42.951,16.963z"/>
|
||||
<path d="M48.265,21.134l-1.14,0.585l-2.797-5.449l1.14-0.585L48.265,21.134z"/>
|
||||
<path d="M53.806,18.069c-0.244,0.616-0.769,1.092-1.575,1.428c-0.807,0.335-1.515,0.373-2.123,0.111 c-0.796-0.3-1.405-0.958-1.826-1.971c-0.43-1.034-0.467-1.928-0.111-2.685c0.244-0.616,0.769-1.092,1.575-1.428 s1.514-0.373,2.123-0.111c0.785,0.282,1.392,0.939,1.822,1.973C54.113,16.399,54.151,17.294,53.806,18.069z M52.707,17.511 c0.13-0.456,0.064-1-0.199-1.633c-0.262-0.629-0.601-1.06-1.017-1.29c-0.417-0.23-0.857-0.249-1.322-0.055 c-0.465,0.194-0.764,0.519-0.898,0.977s-0.069,1.003,0.193,1.635c0.263,0.632,0.604,1.062,1.024,1.29 c0.42,0.228,0.861,0.245,1.326,0.051C52.279,18.292,52.577,17.967,52.707,17.511z"/>
|
||||
<path d="M54.397,11.942l1.28-0.375l3.48,3.403l-1.191-4.073l1.14-0.333l1.72,5.878L59.604,16.8l-3.561-3.459l1.213,4.146 l-1.14,0.333L54.397,11.942z"/>
|
||||
<path d="M65.833,10.335l-3.104,0.573l0.235,1.275l2.843-0.525L66,12.703l-2.843,0.524l0.287,1.552l3.257-0.602l0.199,1.076 l-4.487,0.828l-1.112-6.023l4.333-0.8L65.833,10.335z"/>
|
||||
<path d="M67.808,8.965l1.33-0.107l2.718,4.039l-0.341-4.23l1.184-0.095l0.492,6.105l-1.269,0.102l-2.785-4.109l0.347,4.306 L68.3,15.07L67.808,8.965z"/>
|
||||
<path d="M78.854,14.759l-1.279-0.055l0.262-6.119l1.279,0.055L78.854,14.759z"/>
|
||||
<path d="M81.168,8.739l1.324,0.159l1.862,4.497l0.505-4.213l1.18,0.141l-0.729,6.082l-1.265-0.151l-1.914-4.581l-0.514,4.289 l-1.18-0.141L81.168,8.739z"/>
|
||||
<path d="M88.194,9.732l4.24,0.942l-0.23,1.037l-2.988-0.665l-0.31,1.388l2.623,0.583l-0.229,1.037l-2.623-0.583l-0.557,2.501 l-1.252-0.278L88.194,9.732z"/>
|
||||
<path d="M96.917,17.911c-0.588,0.305-1.297,0.318-2.125,0.042c-0.828-0.276-1.388-0.713-1.674-1.311 c-0.402-0.75-0.431-1.645-0.082-2.686c0.354-1.062,0.914-1.761,1.678-2.097c0.59-0.305,1.298-0.319,2.127-0.042 c0.828,0.276,1.387,0.713,1.674,1.311c0.406,0.728,0.434,1.623,0.079,2.685C98.245,16.854,97.687,17.553,96.917,17.911z M96.456,16.769c0.396-0.258,0.705-0.712,0.922-1.362c0.215-0.647,0.242-1.194,0.08-1.641c-0.162-0.447-0.482-0.75-0.961-0.91 c-0.478-0.159-0.916-0.11-1.318,0.147c-0.401,0.257-0.711,0.71-0.928,1.36c-0.217,0.65-0.242,1.198-0.076,1.645 c0.168,0.447,0.49,0.75,0.968,0.91S96.057,17.027,96.456,16.769z"/>
|
||||
<path d="M104.888,15.472c0.164,0.182,0.276,0.392,0.341,0.628c0.053,0.195,0.074,0.394,0.064,0.596 c-0.012,0.202-0.066,0.413-0.168,0.631c-0.121,0.264-0.309,0.492-0.559,0.685c-0.251,0.193-0.554,0.272-0.908,0.236 c0.207,0.222,0.32,0.452,0.34,0.691c0.019,0.239-0.062,0.554-0.242,0.945l-0.174,0.374c-0.117,0.254-0.188,0.432-0.209,0.532 c-0.037,0.158-0.016,0.296,0.063,0.415l-0.063,0.14l-1.274-0.588c0.022-0.14,0.044-0.25,0.063-0.333 c0.043-0.17,0.101-0.337,0.174-0.502l0.23-0.521c0.159-0.357,0.208-0.623,0.145-0.796c-0.062-0.173-0.258-0.334-0.582-0.484 l-1.137-0.526l-1.01,2.185l-1.135-0.524l2.568-5.56l2.658,1.229C104.454,15.106,104.724,15.289,104.888,15.472z M102.106,15.184 l-0.688,1.489l1.252,0.578c0.248,0.115,0.449,0.171,0.602,0.168c0.27-0.003,0.475-0.161,0.618-0.472 c0.155-0.336,0.153-0.611-0.007-0.825c-0.09-0.121-0.254-0.236-0.492-0.346L102.106,15.184z"/>
|
||||
<path d="M111.327,18.894l1.566,0.953l-3.18,5.235l-1.014-0.616l2.149-3.541c0.062-0.102,0.149-0.244,0.263-0.426 c0.114-0.182,0.201-0.323,0.262-0.422l-3.656,3.792l-1.062-0.644l1.687-4.99c-0.062,0.1-0.146,0.242-0.256,0.427 c-0.107,0.185-0.193,0.328-0.256,0.43l-2.149,3.541l-1.015-0.616l3.179-5.235l1.586,0.963l-1.55,4.693L111.327,18.894z"/>
|
||||
<path d="M116.072,22.188l1.153,0.892l-2.041,6.165l-1.096-0.847l0.449-1.233l-1.783-1.379l-1.09,0.737l-1.065-0.825L116.072,22.188 z M113.683,25.16l1.237,0.957l0.865-2.379L113.683,25.16z"/>
|
||||
<path d="M123.066,28.23l-0.724,0.778l-1.352-1.255l-3.445,3.708l-0.938-0.872l3.446-3.708l-1.352-1.255l0.725-0.778L123.066,28.23z "/>
|
||||
<path d="M120.504,34.474l-0.875-0.936l4.475-4.182l0.875,0.936L120.504,34.474z"/>
|
||||
<path d="M125.021,39.041c-0.66-0.067-1.261-0.443-1.804-1.128c-0.541-0.685-0.77-1.356-0.684-2.013 c0.073-0.848,0.541-1.612,1.401-2.293c0.878-0.695,1.729-0.973,2.553-0.836c0.659,0.067,1.261,0.443,1.804,1.128 c0.541,0.685,0.77,1.356,0.684,2.013c-0.059,0.832-0.525,1.595-1.403,2.29C126.709,38.882,125.86,39.162,125.021,39.041z M125.258,37.832c0.476,0.001,0.98-0.21,1.519-0.635c0.534-0.423,0.856-0.866,0.966-1.329c0.107-0.463,0.006-0.892-0.307-1.287 s-0.707-0.595-1.186-0.6c-0.477-0.005-0.982,0.206-1.521,0.63c-0.538,0.424-0.858,0.87-0.963,1.335 c-0.104,0.465-0.002,0.896,0.312,1.29C124.391,37.632,124.784,37.83,125.258,37.832z"/>
|
||||
<path d="M131.081,37.952l0.705,1.131l-2.334,4.271l3.6-2.249l0.629,1.007l-5.194,3.246l-0.674-1.079l2.367-4.364l-3.664,2.29 l-0.629-1.007L131.081,37.952z"/>
|
||||
<path d="M131.352,46.758c-0.232,0.158-0.387,0.318-0.46,0.481c-0.134,0.297-0.089,0.673,0.135,1.128 c0.134,0.272,0.271,0.479,0.414,0.62c0.271,0.266,0.562,0.324,0.87,0.172c0.182-0.088,0.281-0.236,0.303-0.443 c0.02-0.206-0.019-0.498-0.113-0.877l-0.156-0.65c-0.154-0.639-0.211-1.106-0.168-1.403c0.066-0.5,0.369-0.882,0.908-1.146 c0.49-0.24,0.985-0.259,1.486-0.057c0.502,0.202,0.924,0.653,1.268,1.354c0.287,0.584,0.377,1.158,0.272,1.722 c-0.104,0.563-0.442,0.998-1.019,1.304l-0.543-1.108c0.32-0.182,0.484-0.443,0.49-0.784c0.002-0.227-0.062-0.475-0.194-0.745 c-0.147-0.3-0.324-0.511-0.531-0.631c-0.207-0.121-0.417-0.129-0.63-0.025c-0.195,0.096-0.299,0.256-0.309,0.48 c-0.01,0.146,0.033,0.424,0.129,0.835l0.242,1.067c0.106,0.468,0.129,0.848,0.065,1.14c-0.099,0.453-0.39,0.8-0.878,1.039 c-0.501,0.245-1.012,0.256-1.531,0.032c-0.521-0.224-0.953-0.688-1.298-1.393c-0.353-0.72-0.467-1.366-0.342-1.938 c0.124-0.573,0.473-0.999,1.044-1.279L131.352,46.758z"/>
|
||||
<path d="M139.122,54.22l-0.994,0.375l-0.651-1.725l-4.736,1.787l-0.452-1.199l4.736-1.787l-0.65-1.725l0.994-0.375L139.122,54.22z"/>
|
||||
<path d="M139.737,60.395l-0.83-3.045l-1.251,0.341l0.761,2.789l-1.025,0.279l-0.76-2.789l-1.522,0.415l0.871,3.196l-1.056,0.288 l-1.199-4.402l5.908-1.611l1.158,4.251L139.737,60.395z"/>
|
||||
<path d="M141.273,62.279l0.204,1.233l-2.493,0.413l2.881,1.925l0.271,1.627l-2.896-2.068l-3.126,3.195l-0.268-1.62l2.331-2.3 l-0.721-0.507l-2.021,0.334l-0.204-1.233L141.273,62.279z"/>
|
||||
<path d="M142.313,69.3l0.077,1.331l-4.099,2.627l4.236-0.246l0.069,1.185l-6.114,0.356l-0.074-1.271l4.172-2.693l-4.313,0.251 l-0.067-1.186L142.313,69.3z"/>
|
||||
<path d="M136.42,77.345l0.022-1.281l6.124,0.11l-0.023,1.281L136.42,77.345z"/>
|
||||
<path d="M142.495,79.542l-0.119,1.244l-2.516-0.238l2.293,2.598l-0.156,1.643l-2.271-2.738l-3.839,2.289l0.154-1.635l2.842-1.627 l-0.565-0.675l-2.04-0.192l0.118-1.244L142.495,79.542z"/>
|
||||
</g>
|
||||
<rect x="13.962" y="14.947" fill="none" width="121.094" height="121.095"/>
|
||||
<g>
|
||||
<path d="M14.433,92.2c-0.122,0.209-0.31,0.348-0.566,0.416c-0.256,0.065-0.49,0.038-0.702-0.084 c-0.212-0.123-0.351-0.312-0.418-0.568s-0.039-0.488,0.085-0.699s0.315-0.35,0.571-0.417c0.256-0.067,0.488-0.039,0.697,0.084 c0.209,0.124,0.348,0.313,0.415,0.569C14.583,91.756,14.555,91.989,14.433,92.2z"/>
|
||||
<path d="M17.734,96.76c0.649,0.271,1.115,0.759,1.398,1.464c0.377,0.942,0.34,1.76-0.112,2.446 c-0.256,0.383-0.558,0.654-0.905,0.812l-0.476-1.189c0.242-0.184,0.409-0.361,0.5-0.535c0.163-0.309,0.161-0.67-0.004-1.082 c-0.168-0.42-0.475-0.684-0.92-0.786c-0.445-0.103-0.985-0.027-1.621,0.228c-0.636,0.254-1.062,0.572-1.276,0.954 c-0.215,0.382-0.244,0.771-0.086,1.162c0.162,0.403,0.421,0.655,0.78,0.759c0.194,0.057,0.454,0.065,0.779,0.024l0.476,1.188 c-0.659,0.146-1.265,0.088-1.819-0.174c-0.554-0.263-0.962-0.72-1.223-1.371c-0.323-0.807-0.315-1.545,0.023-2.216 c0.341-0.672,0.969-1.19,1.884-1.558C16.121,96.493,16.988,96.45,17.734,96.76z"/>
|
||||
<path d="M15.157,104.76l5.418-2.856l0.597,1.134l-2.059,1.086l1.093,2.073l2.06-1.086l0.598,1.134l-5.418,2.855l-0.598-1.133 l2.419-1.275l-1.093-2.072l-2.418,1.274L15.157,104.76z"/>
|
||||
<path d="M24.609,108.986l0.812,1.211l-3.888,5.201l-0.771-1.149l0.816-1.027l-1.254-1.872l-1.268,0.354l-0.75-1.121L24.609,108.986 z M21.401,111.047l0.871,1.301l1.575-1.982L21.401,111.047z"/>
|
||||
<path d="M27.297,112.821l0.813,0.989l-3.886,3.194l1.925,2.343l-0.845,0.694l-2.739-3.332L27.297,112.821z"/>
|
||||
<path d="M33.701,119.881l1.308,1.287l-4.295,4.365l-0.847-0.832l2.905-2.953c0.083-0.086,0.201-0.203,0.354-0.354 c0.152-0.151,0.27-0.269,0.351-0.353l-4.43,2.854l-0.885-0.87l2.786-4.471c-0.082,0.083-0.197,0.202-0.346,0.356 c-0.149,0.155-0.265,0.274-0.348,0.359l-2.905,2.953l-0.846-0.832l4.295-4.366l1.323,1.302l-2.584,4.213L33.701,119.881z"/>
|
||||
<path d="M38.937,125.708l-2.477-1.957l-0.804,1.019l2.269,1.792l-0.659,0.834l-2.268-1.792l-0.979,1.237l2.599,2.054l-0.678,0.858 l-3.58-2.828l3.797-4.807l3.457,2.731L38.937,125.708z"/>
|
||||
<path d="M44.062,127.978c0.138,0.203,0.222,0.426,0.253,0.669c0.026,0.2,0.02,0.4-0.018,0.6c-0.038,0.198-0.121,0.399-0.251,0.604 c-0.156,0.245-0.372,0.445-0.646,0.604c-0.275,0.157-0.585,0.194-0.932,0.11c0.176,0.247,0.257,0.491,0.243,0.729 c-0.014,0.24-0.137,0.541-0.369,0.903l-0.222,0.349c-0.151,0.234-0.244,0.401-0.279,0.498c-0.057,0.15-0.055,0.291,0.007,0.42 l-0.083,0.129l-1.183-0.756c0.041-0.135,0.077-0.241,0.108-0.32c0.065-0.163,0.145-0.321,0.24-0.475l0.3-0.484 c0.206-0.333,0.29-0.59,0.25-0.77c-0.04-0.181-0.209-0.367-0.511-0.56l-1.056-0.675l-1.296,2.027l-1.053-0.674l3.298-5.16 l2.468,1.578C43.681,127.557,43.924,127.774,44.062,127.978z M41.345,127.314l-0.883,1.383l1.162,0.743 c0.23,0.146,0.421,0.23,0.573,0.248c0.267,0.032,0.492-0.096,0.677-0.384c0.199-0.312,0.234-0.584,0.105-0.817 c-0.073-0.132-0.22-0.27-0.441-0.41L41.345,127.314z"/>
|
||||
<path d="M45.217,133.312c-0.091,0.267-0.117,0.485-0.078,0.66c0.073,0.317,0.334,0.591,0.786,0.819 c0.271,0.138,0.505,0.22,0.704,0.246c0.377,0.049,0.644-0.08,0.799-0.387c0.091-0.18,0.083-0.357-0.025-0.535 c-0.109-0.177-0.315-0.387-0.619-0.634l-0.517-0.424c-0.507-0.417-0.834-0.757-0.979-1.021c-0.247-0.439-0.235-0.928,0.035-1.461 c0.247-0.488,0.632-0.802,1.154-0.941c0.522-0.141,1.131-0.034,1.826,0.318c0.581,0.295,0.999,0.697,1.254,1.211 c0.255,0.513,0.246,1.063-0.028,1.652l-1.101-0.559c0.146-0.338,0.119-0.646-0.082-0.92c-0.134-0.184-0.335-0.342-0.604-0.479 c-0.298-0.15-0.566-0.213-0.804-0.186s-0.411,0.147-0.518,0.359c-0.099,0.194-0.084,0.385,0.042,0.568 c0.081,0.121,0.282,0.316,0.605,0.59l0.836,0.706c0.367,0.31,0.613,0.601,0.739,0.87c0.195,0.421,0.169,0.873-0.077,1.357 c-0.252,0.498-0.653,0.812-1.204,0.947c-0.55,0.135-1.175,0.023-1.875-0.332c-0.715-0.361-1.195-0.81-1.44-1.342 c-0.245-0.531-0.223-1.082,0.064-1.65L45.217,133.312z"/>
|
||||
<path d="M58.45,134.226l-0.344,1.005l-1.744-0.598l-1.642,4.787l-1.212-0.414l1.642-4.789l-1.744-0.598l0.345-1.006L58.45,134.226z "/>
|
||||
<path d="M63.51,136.602l-3.069-0.738l-0.303,1.262l2.811,0.676l-0.249,1.033l-2.811-0.676l-0.369,1.533l3.221,0.773l-0.256,1.063 l-4.436-1.067l1.433-5.953l4.284,1.029L63.51,136.602z"/>
|
||||
<path d="M65.268,135.827l1.238,0.168l-0.338,2.504l2.687-2.188l1.635,0.223l-2.828,2.158l2.135,3.928l-1.627-0.221l-1.513-2.904 l-0.697,0.539l-0.274,2.029l-1.239-0.168L65.268,135.827z"/>
|
||||
<path d="M71.6,136.6l1.333,0.039l2.259,4.312l0.125-4.242l1.188,0.035l-0.18,6.123l-1.272-0.037l-2.317-4.392l-0.127,4.317 l-1.187-0.034L71.6,136.6z"/>
|
||||
<path d="M79.522,142.725l-1.279,0.059l-0.277-6.119l1.279-0.059L79.522,142.725z"/>
|
||||
<path d="M82.211,140.633c0.069,0.271,0.17,0.47,0.3,0.593c0.235,0.226,0.604,0.31,1.108,0.249c0.301-0.035,0.541-0.096,0.723-0.184 c0.341-0.168,0.492-0.422,0.451-0.765c-0.022-0.2-0.129-0.344-0.315-0.433c-0.188-0.088-0.478-0.148-0.865-0.186l-0.666-0.068 c-0.653-0.066-1.112-0.17-1.379-0.31c-0.448-0.229-0.709-0.643-0.778-1.237c-0.062-0.543,0.084-1.017,0.441-1.422 c0.357-0.406,0.924-0.654,1.698-0.744c0.646-0.076,1.218,0.029,1.714,0.315s0.793,0.751,0.891,1.394l-1.226,0.145 c-0.065-0.363-0.257-0.604-0.576-0.723c-0.213-0.078-0.469-0.1-0.769-0.064c-0.332,0.039-0.59,0.137-0.771,0.29 c-0.184,0.155-0.261,0.351-0.232,0.586c0.024,0.218,0.143,0.367,0.351,0.451c0.134,0.058,0.41,0.108,0.83,0.155l1.087,0.127 c0.477,0.055,0.843,0.16,1.098,0.316c0.396,0.243,0.623,0.635,0.688,1.174c0.064,0.555-0.096,1.039-0.479,1.455 c-0.386,0.416-0.968,0.67-1.746,0.76c-0.797,0.094-1.443-0.014-1.941-0.322s-0.784-0.777-0.858-1.41L82.211,140.633z"/>
|
||||
<path d="M86.725,135.772l1.221-0.27l0.543,2.469l1.771-2.979l1.611-0.354l-1.914,3l3.355,2.953l-1.604,0.354l-2.42-2.207 l-0.47,0.745l0.44,2.001l-1.221,0.27L86.725,135.772z"/>
|
||||
<path d="M94.597,133.771l1.384-0.459l3.979,5.133l-1.313,0.437l-0.772-1.061l-2.14,0.71l0.004,1.316l-1.28,0.425L94.597,133.771z M95.729,137.413l1.484-0.494l-1.491-2.047L95.729,137.413z"/>
|
||||
<path d="M104.025,136.72l-2.748-5.476l1.146-0.574l1.045,2.082l2.095-1.053l-1.044-2.08l1.145-0.574l2.748,5.474l-1.146,0.575 l-1.228-2.444l-2.095,1.052l1.228,2.443L104.025,136.72z"/>
|
||||
<path d="M107.144,126.129l0.918-0.594l0.535,0.827l-0.919,0.595L107.144,126.129z M113.952,130.424 c-0.129,0.65-0.562,1.212-1.295,1.687c-0.732,0.474-1.423,0.638-2.068,0.488c-0.836-0.154-1.553-0.69-2.148-1.613 c-0.607-0.94-0.804-1.813-0.588-2.622c0.129-0.648,0.561-1.212,1.294-1.686c0.733-0.475,1.424-0.637,2.069-0.488 c0.822,0.137,1.537,0.676,2.146,1.615C113.956,128.727,114.153,129.6,113.952,130.424z M108.547,125.223l0.918-0.594l0.535,0.826 l-0.918,0.594L108.547,125.223z M112.771,130.071c0.047-0.473-0.115-0.996-0.487-1.57c-0.369-0.572-0.779-0.937-1.229-1.088 c-0.451-0.152-0.888-0.093-1.312,0.182c-0.423,0.273-0.658,0.646-0.709,1.121c-0.051,0.476,0.11,1,0.481,1.574 c0.372,0.576,0.784,0.938,1.237,1.086s0.892,0.088,1.313-0.187S112.725,130.543,112.771,130.071z"/>
|
||||
<path d="M117.1,123.377c-0.342-0.262-0.709-0.338-1.104-0.229c-0.222,0.062-0.432,0.176-0.629,0.337 c-0.381,0.312-0.576,0.711-0.586,1.198c-0.01,0.486,0.221,1.018,0.692,1.592c0.476,0.579,0.942,0.881,1.403,0.903 c0.463,0.022,0.861-0.103,1.199-0.38c0.332-0.271,0.524-0.588,0.582-0.95c0.055-0.362-0.018-0.718-0.22-1.063l-1.103,0.903 l-0.646-0.785l1.97-1.614l2.08,2.537l-0.652,0.535l-0.575-0.518c-0.007,0.38-0.05,0.678-0.125,0.894 c-0.132,0.374-0.386,0.718-0.767,1.029c-0.627,0.515-1.318,0.719-2.074,0.612c-0.773-0.094-1.471-0.518-2.09-1.272 c-0.627-0.765-0.92-1.55-0.877-2.354c0.043-0.806,0.412-1.494,1.104-2.062c0.602-0.491,1.208-0.735,1.82-0.729 c0.615,0.006,1.135,0.215,1.561,0.625L117.1,123.377z"/>
|
||||
<path d="M121.618,122.366c0.225,0.168,0.426,0.26,0.604,0.274c0.324,0.028,0.665-0.14,1.021-0.499 c0.213-0.217,0.361-0.414,0.447-0.596c0.162-0.344,0.12-0.637-0.124-0.879c-0.144-0.142-0.315-0.188-0.519-0.14 c-0.2,0.05-0.465,0.183-0.791,0.396l-0.562,0.363c-0.553,0.356-0.976,0.564-1.271,0.623c-0.494,0.102-0.954-0.06-1.381-0.479 c-0.39-0.384-0.57-0.847-0.545-1.386c0.024-0.541,0.312-1.088,0.858-1.644c0.457-0.464,0.97-0.739,1.534-0.825 c0.566-0.089,1.089,0.088,1.566,0.528l-0.867,0.879c-0.276-0.241-0.577-0.31-0.9-0.202c-0.215,0.072-0.428,0.215-0.641,0.43 c-0.233,0.238-0.375,0.476-0.42,0.709c-0.047,0.235,0.016,0.438,0.185,0.604c0.156,0.152,0.341,0.198,0.556,0.133 c0.141-0.039,0.389-0.172,0.746-0.396l0.928-0.582c0.406-0.254,0.758-0.4,1.054-0.438c0.46-0.058,0.884,0.104,1.271,0.486 c0.397,0.393,0.575,0.869,0.536,1.436c-0.04,0.563-0.336,1.125-0.887,1.685c-0.562,0.571-1.136,0.892-1.718,0.963 c-0.58,0.071-1.098-0.116-1.551-0.563L121.618,122.366z"/>
|
||||
<path d="M122.098,115.749l0.783-0.975l1.97,1.584l-0.483-3.432l1.033-1.284l0.389,3.537l4.465,0.2l-1.027,1.279l-3.27-0.205 l0.102,0.875l1.597,1.283l-0.782,0.975L122.098,115.749z"/>
|
||||
<path d="M133.273,109.766c0.151,0.646-0.009,1.336-0.479,2.07c-0.472,0.735-1.031,1.17-1.682,1.302 c-0.824,0.207-1.699,0.014-2.624-0.578c-0.942-0.604-1.483-1.317-1.62-2.144c-0.152-0.646,0.008-1.336,0.479-2.07 c0.471-0.735,1.03-1.17,1.681-1.301c0.807-0.217,1.68-0.022,2.622,0.582C132.575,108.219,133.116,108.932,133.273,109.766z M132.053,109.934c-0.152-0.449-0.518-0.857-1.094-1.229c-0.574-0.369-1.099-0.529-1.571-0.48 c-0.474,0.049-0.847,0.283-1.117,0.708c-0.271,0.424-0.332,0.862-0.183,1.313c0.15,0.453,0.516,0.864,1.092,1.233 c0.576,0.37,1.102,0.528,1.576,0.478c0.474-0.051,0.847-0.291,1.119-0.715C132.146,110.819,132.206,110.383,132.053,109.934z"/>
|
||||
<path d="M129.695,104.815l0.579-1.143l4.486,2.277l1.371-2.703l0.977,0.494l-1.952,3.846L129.695,104.815z"/>
|
||||
<path d="M132.765,98.278l0.53-1.357l6.49,0.221l-0.505,1.289l-1.31-0.083l-0.82,2.1l1.016,0.839l-0.492,1.256L132.765,98.278z M136.29,99.733l0.568-1.457l-2.527-0.16L136.29,99.733z"/>
|
||||
<path d="M137.782,90.629c0.208-0.125,0.44-0.153,0.697-0.088c0.256,0.065,0.447,0.205,0.572,0.414 c0.126,0.211,0.154,0.443,0.089,0.699c-0.065,0.258-0.204,0.446-0.415,0.57c-0.212,0.124-0.445,0.152-0.702,0.087 c-0.256-0.065-0.444-0.204-0.567-0.413c-0.123-0.21-0.15-0.443-0.086-0.699C137.437,90.944,137.575,90.754,137.782,90.629z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 23 KiB |
BIN
frontend/static/images/default_covers/00.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/01.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/02.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/03.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/04.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/05.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/06.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/07.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/08.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/09.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/10.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/11.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/12.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/13.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/14.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/15.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/16.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/17.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/18.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/19.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/20.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/21.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/22.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/23.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/24.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
@ -18,6 +18,17 @@ body {
|
||||
font-family: Ubuntu,serif;
|
||||
}
|
||||
|
||||
body::after {
|
||||
display: absolute;
|
||||
top: 0;
|
||||
bottom: 100vh;
|
||||
left: 0;
|
||||
right: 100vw;
|
||||
|
||||
background-image: url(/images/background.svg);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.nobr {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -38,7 +49,7 @@ body {
|
||||
.song_search_bar {
|
||||
position: relative;
|
||||
max-width: 38em;
|
||||
width: 80%;
|
||||
width: 95%;
|
||||
margin: auto;
|
||||
margin-top: 1em;
|
||||
}
|
||||
252
src/app.rs
@ -1,252 +0,0 @@
|
||||
use crate::css::C;
|
||||
use crate::query::ParsedQuery;
|
||||
use crate::song::Song;
|
||||
use anyhow::anyhow;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use seed::app::cmds::timeout;
|
||||
use seed::browser::util::document;
|
||||
use seed::prelude::*;
|
||||
use seed::{attrs, button, div, empty, error, img, input, p, span, C, IF};
|
||||
use std::cmp::Reverse;
|
||||
use web_sys::Element;
|
||||
|
||||
pub struct Model {
|
||||
songs: Vec<Song>,
|
||||
query: String,
|
||||
filtered_songs: Vec<usize>,
|
||||
hidden_songs: usize,
|
||||
shown_songs: usize,
|
||||
filter_video: bool,
|
||||
filter_duets: bool,
|
||||
}
|
||||
|
||||
const SCROLL_THRESHOLD: usize = 50;
|
||||
const INITIAL_ELEM_COUNT: usize = 100;
|
||||
|
||||
pub enum Msg {
|
||||
Search(String),
|
||||
ToggleVideo,
|
||||
ToggleDuets,
|
||||
Shuffle,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
|
||||
let songs: Vec<Song> =
|
||||
serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs");
|
||||
let mut filtered_songs: Vec<usize> = (0..songs.len()).collect();
|
||||
filtered_songs.shuffle(&mut thread_rng());
|
||||
|
||||
Model {
|
||||
songs,
|
||||
query: String::new(),
|
||||
filtered_songs,
|
||||
hidden_songs: 0,
|
||||
shown_songs: INITIAL_ELEM_COUNT,
|
||||
filter_video: false,
|
||||
filter_duets: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Search(query) => {
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
|
||||
model.query = query;
|
||||
|
||||
if model.query.is_empty() {
|
||||
model.filter_duets = false;
|
||||
model.filter_video = false;
|
||||
update(Msg::Shuffle, model, orders)
|
||||
} else {
|
||||
let query = ParsedQuery::parse(&model.query);
|
||||
model.filtered_songs.sort_by_cached_key(|&i| {
|
||||
let song = &model.songs[i];
|
||||
let score = song.fuzzy_compare(&query);
|
||||
if score < Default::default() {
|
||||
model.hidden_songs += 1;
|
||||
}
|
||||
|
||||
let top_score = Reverse(score);
|
||||
|
||||
(top_score, &song.title, &song.artist, &song.song_hash)
|
||||
});
|
||||
model.filter_duets = query.duet == Some(true);
|
||||
model.filter_video = query.video == Some(true);
|
||||
}
|
||||
}
|
||||
Msg::ToggleVideo => {
|
||||
let mut query = ParsedQuery::parse(&model.query);
|
||||
query.video = match query.video {
|
||||
Some(true) => None,
|
||||
None | Some(false) => Some(true),
|
||||
};
|
||||
update(Msg::Search(query.to_string()), model, orders);
|
||||
}
|
||||
Msg::ToggleDuets => {
|
||||
let mut query = ParsedQuery::parse(&model.query);
|
||||
query.duet = match query.duet {
|
||||
Some(true) => None,
|
||||
None | Some(false) => Some(true),
|
||||
};
|
||||
update(Msg::Search(query.to_string()), model, orders);
|
||||
}
|
||||
Msg::Shuffle => {
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
model.query.clear();
|
||||
model.filtered_songs.shuffle(&mut thread_rng());
|
||||
}
|
||||
Msg::Scroll => {
|
||||
let (scroll, max_scroll) = match get_scroll() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_left: i32 = max_scroll - scroll;
|
||||
|
||||
// when there are fewer elements than this below the scroll viewport, add more
|
||||
const ELEMENT_HEIGHT: i32 = 48;
|
||||
|
||||
if scroll_left < ELEMENT_HEIGHT * SCROLL_THRESHOLD as i32 {
|
||||
model.shown_songs += 1;
|
||||
orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
let song_card = |song: &Song| -> Node<Msg> {
|
||||
div![
|
||||
C![C.song_item],
|
||||
img![
|
||||
C![C.song_item_cover],
|
||||
match song.cover {
|
||||
Some(_) => attrs! {At::Src => format!("/images/songs/{}.png", song.song_hash)},
|
||||
None => attrs! {At::Src => "/images/default_cover.png"},
|
||||
},
|
||||
],
|
||||
div![
|
||||
C![C.song_item_info],
|
||||
div![C![C.song_item_title], song.title.to_string()],
|
||||
div![C![C.song_item_artist], song.artist.to_string()],
|
||||
],
|
||||
div![
|
||||
C![C.song_gizmos],
|
||||
match &song.genre {
|
||||
Some(genre) => div![
|
||||
C![C.gizmo, C.note_icon, C.tooltip],
|
||||
span![C![C.tooltiptext], genre],
|
||||
],
|
||||
None => empty![],
|
||||
},
|
||||
match &song.language {
|
||||
Some(language) => div![
|
||||
C![C.gizmo, C.flag_icon, C.tooltip],
|
||||
span![C![C.tooltiptext], language],
|
||||
],
|
||||
None => empty![],
|
||||
},
|
||||
IF![song.video.is_some() => div![
|
||||
C![C.gizmo, C.video_icon, C.tooltip],
|
||||
span![
|
||||
C![C.tooltiptext],
|
||||
"Musikvideo",
|
||||
],
|
||||
]],
|
||||
match (&song.duet_singer_1, &song.duet_singer_2) {
|
||||
(Some(p1), Some(p2)) => div![
|
||||
C![C.gizmo, C.duet_icon, C.tooltip],
|
||||
span![
|
||||
C![C.tooltiptext],
|
||||
"Duet",
|
||||
div![
|
||||
C![C.marquee],
|
||||
p![" 🗲 ", p1, " 🗲 ", p2, " 🗲 ", p1, " 🗲 ", p2]
|
||||
],
|
||||
],
|
||||
],
|
||||
_ => empty![],
|
||||
},
|
||||
],
|
||||
]
|
||||
};
|
||||
|
||||
vec![
|
||||
div![
|
||||
C![C.song_search_bar],
|
||||
input![
|
||||
input_ev(Ev::Input, Msg::Search),
|
||||
attrs! {
|
||||
At::Placeholder => "Sök",
|
||||
At::Value => model.query,
|
||||
},
|
||||
C![C.song_search_field],
|
||||
],
|
||||
button![
|
||||
C![C.song_sort_button, C.tooltip],
|
||||
IF![model.filter_duets => C![C.song_sort_button_selected]],
|
||||
ev(Ev::Click, |_| Msg::ToggleDuets),
|
||||
span![C![C.tooltiptext], "Endast Duetter"],
|
||||
"D",
|
||||
],
|
||||
button![
|
||||
C![C.song_sort_button, C.tooltip],
|
||||
IF![model.filter_video => C![C.song_sort_button_selected]],
|
||||
ev(Ev::Click, |_| Msg::ToggleVideo),
|
||||
span![C![C.tooltiptext], "Endast med Video"],
|
||||
"V",
|
||||
],
|
||||
button![
|
||||
C![C.song_sort_button, C.song_sort_button_right, C.tooltip],
|
||||
IF![model.filter_video => C![C.song_sort_button_selected]],
|
||||
ev(Ev::Click, |_| Msg::Shuffle),
|
||||
span![C![C.tooltiptext], "Blanda låtar"],
|
||||
"🔀",
|
||||
],
|
||||
],
|
||||
div![
|
||||
C![C.song_list],
|
||||
attrs! {At::Id => SONG_LIST_ID},
|
||||
ev(Ev::Scroll, |_| Msg::Scroll),
|
||||
model
|
||||
.filtered_songs
|
||||
.iter()
|
||||
.map(|&i| &model.songs[i])
|
||||
.map(song_card)
|
||||
.take(model.filtered_songs.len() - model.hidden_songs)
|
||||
.take(model.shown_songs),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const SONG_LIST_ID: &str = "song_list";
|
||||
|
||||
fn get_song_list_element() -> anyhow::Result<Element> {
|
||||
document()
|
||||
.get_element_by_id(SONG_LIST_ID)
|
||||
.ok_or(anyhow!("Failed to access song list element"))
|
||||
}
|
||||
|
||||
fn scroll_to_top() {
|
||||
if let Ok(elem) = get_song_list_element() {
|
||||
elem.scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scroll() -> anyhow::Result<(i32, i32)> {
|
||||
let list = get_song_list_element()?;
|
||||
let scroll = list.scroll_top();
|
||||
let height = list.client_height();
|
||||
let max = (list.scroll_height() - height).max(0);
|
||||
Ok((scroll, max))
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |