Initial import
This commit is contained in:
commit
1ff831f80d
15
bin/app.psgi
Executable file
15
bin/app.psgi
Executable file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
|
||||
use PoorBooru;
|
||||
use PoorBooru::API::V0;
|
||||
use Plack::Builder;
|
||||
|
||||
builder {
|
||||
mount "/" => PoorBooru->to_app;
|
||||
mount "/api/v0" => PoorBooru::API::V0->to_app;
|
||||
};
|
62
bin/create-dbix-class-schemas
Executable file
62
bin/create-dbix-class-schemas
Executable file
@ -0,0 +1,62 @@
|
||||
#!/bin/sh
|
||||
err()
|
||||
{
|
||||
printf "%s: %s\n" "${0##*/}" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
warn()
|
||||
{
|
||||
printf "%s: %s\n" "${0##*/}" "$*" >&2
|
||||
}
|
||||
|
||||
check_required_programs()
|
||||
{
|
||||
_rc=0
|
||||
for _prog; do
|
||||
if ! command -v "$_prog" >/dev/null; then
|
||||
_rc=1
|
||||
warn "$_prog: not found"
|
||||
fi
|
||||
done
|
||||
return $_rc;
|
||||
}
|
||||
|
||||
set -eu
|
||||
|
||||
check_required_programs sqlite3 dbicdump || exit 1
|
||||
|
||||
sqlite3 "db/PoorBooru.db" <<'_SQL'
|
||||
PRAGMA foreig_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media(
|
||||
media_id INTEGER PRIMARY KEY,
|
||||
content BLOB (10485760) NOT NULL,
|
||||
filename TEXT (255) NOT NULL,
|
||||
content_type TEXT (255)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags(
|
||||
tag_id INTEGER PRIMARY KEY,
|
||||
name TEXT (255) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_tags(
|
||||
media_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (media_id) REFERENCES media (media_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags (tag_id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(media_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE VIEW IF NOT EXISTS tags_count_view AS SELECT
|
||||
tags.tag_id AS tag_id,
|
||||
tags.name AS name,
|
||||
COUNT(media_tags.media_id) AS count
|
||||
FROM tags
|
||||
INNER JOIN media_tags USING (tag_id)
|
||||
GROUP BY tag_id;
|
||||
|
||||
_SQL
|
||||
|
||||
dbicdump -o dump_directory=./lib PoorBooru::Schema dbi:SQLite:db/PoorBooru.db
|
68
config.yml
Normal file
68
config.yml
Normal file
@ -0,0 +1,68 @@
|
||||
# This is the main configuration file of your Dancer2 app
|
||||
# env-related settings should go to environments/$env.yml
|
||||
# all the settings in this file will be loaded at Dancer's startup.
|
||||
|
||||
# === Basic configuration ===
|
||||
|
||||
# Your application's name
|
||||
appname: "PoorBooru"
|
||||
|
||||
# The default layout to use for your application (located in
|
||||
# views/layouts/main.tt)
|
||||
layout: "main"
|
||||
|
||||
# when the charset is set to UTF-8 Dancer2 will handle for you
|
||||
# all the magic of encoding and decoding. You should not care
|
||||
# about unicode within your app when this setting is set (recommended).
|
||||
charset: "UTF-8"
|
||||
|
||||
# === Engines ===
|
||||
#
|
||||
# NOTE: All the engine configurations need to be under a single "engines:"
|
||||
# key. If you uncomment engine configurations below, make sure to delete
|
||||
# all "engines:" lines except the first. Otherwise, only the last
|
||||
# "engines:" block will take effect.
|
||||
|
||||
# template engine
|
||||
# simple: default and very basic template engine
|
||||
# template_toolkit: TT
|
||||
|
||||
template: "tiny"
|
||||
|
||||
# template: "template_toolkit"
|
||||
# engines:
|
||||
# template:
|
||||
# template_toolkit:
|
||||
# # Note: start_tag and end_tag are regexes
|
||||
# start_tag: '<%'
|
||||
# end_tag: '%>'
|
||||
|
||||
# session engine
|
||||
#
|
||||
# Simple: in-memory session store - Dancer2::Session::Simple
|
||||
# YAML: session stored in YAML files - Dancer2::Session::YAML
|
||||
#
|
||||
# Check out metacpan for other session storage options:
|
||||
# https://metacpan.org/search?q=Dancer2%3A%3ASession&search_type=modules
|
||||
#
|
||||
# Default value for 'cookie_name' is 'dancer.session'. If you run multiple
|
||||
# Dancer apps on the same host then you will need to make sure 'cookie_name'
|
||||
# is different for each app.
|
||||
#
|
||||
#engines:
|
||||
# session:
|
||||
# Simple:
|
||||
# cookie_name: testapp.session
|
||||
#
|
||||
#engines:
|
||||
# session:
|
||||
# YAML:
|
||||
# cookie_name: eshop.session
|
||||
# is_secure: 1
|
||||
# is_http_only: 1
|
||||
|
||||
plugins:
|
||||
DBIC:
|
||||
default:
|
||||
dsn: "dbi:SQLite:dbname=db/PoorBooru.db"
|
||||
schema_class: PoorBooru::Schema
|
34
cpanfile
Normal file
34
cpanfile
Normal file
@ -0,0 +1,34 @@
|
||||
requires "Dancer2" => "0.400000";
|
||||
|
||||
recommends "YAML" => "0";
|
||||
recommends "URL::Encode::XS" => "0";
|
||||
recommends "CGI::Deurl::XS" => "0";
|
||||
recommends "CBOR::XS" => "0";
|
||||
recommends "YAML::XS" => "0";
|
||||
recommends "Class::XSAccessor" => "0";
|
||||
recommends "Crypt::URandom" => "0";
|
||||
recommends "HTTP::XSCookies" => "0";
|
||||
recommends "HTTP::XSHeaders" => "0";
|
||||
recommends "Math::Random::ISAAC::XS" => "0";
|
||||
recommends "MooX::TypeTiny" => "0";
|
||||
recommends "Type::Tiny::XS" => "0";
|
||||
|
||||
feature 'accelerate', 'Accelerate Dancer2 app performance with XS modules' => sub {
|
||||
requires "URL::Encode::XS" => "0";
|
||||
requires "CGI::Deurl::XS" => "0";
|
||||
requires "YAML::XS" => "0";
|
||||
requires "Class::XSAccessor" => "0";
|
||||
requires "Cpanel::JSON::XS" => "0";
|
||||
requires "Crypt::URandom" => "0";
|
||||
requires "HTTP::XSCookies" => "0";
|
||||
requires "HTTP::XSHeaders" => "0";
|
||||
requires "Math::Random::ISAAC::XS" => "0";
|
||||
requires "MooX::TypeTiny" => "0";
|
||||
requires "Type::Tiny::XS" => "0";
|
||||
};
|
||||
|
||||
on "test" => sub {
|
||||
requires "Test::More" => "0";
|
||||
requires "HTTP::Request::Common" => "0";
|
||||
};
|
||||
|
22
environments/development.yml
Normal file
22
environments/development.yml
Normal file
@ -0,0 +1,22 @@
|
||||
# configuration file for development environment
|
||||
|
||||
# the logger engine to use
|
||||
# console: log messages to STDOUT (your console where you started the
|
||||
# application server)
|
||||
# file: log message to a file in log/
|
||||
logger: "console"
|
||||
|
||||
# the log level for this environment
|
||||
# core is the lowest, it shows Dancer2's core log messages as well as yours
|
||||
# (debug, info, warning and error)
|
||||
log: "core"
|
||||
|
||||
# should Dancer2 show a stacktrace when an 5xx error is caught?
|
||||
# if set to yes, public/500.html will be ignored and either
|
||||
# views/500.tt, 'error_template' template, or a default error template will be used.
|
||||
show_errors: 1
|
||||
|
||||
# print the banner
|
||||
startup_info: 1
|
||||
|
||||
poorbooru_api: "http://localhost:8080/api/v0"
|
13
environments/production.yml
Normal file
13
environments/production.yml
Normal file
@ -0,0 +1,13 @@
|
||||
# configuration file for production environment
|
||||
|
||||
# only log warning and error messsages
|
||||
log: "warning"
|
||||
|
||||
# log message to a file in logs/
|
||||
logger: "file"
|
||||
|
||||
# hide errors
|
||||
show_errors: 0
|
||||
|
||||
# disable server tokens in production environments
|
||||
no_server_tokens: 1
|
37
lib/PoorBooru.pm
Normal file
37
lib/PoorBooru.pm
Normal file
@ -0,0 +1,37 @@
|
||||
package PoorBooru;
|
||||
use Dancer2;
|
||||
use HTTP::Tiny;
|
||||
|
||||
our $VERSION = v0.1;
|
||||
|
||||
my $POORBOORU_API = setting("poorbooru_api");
|
||||
|
||||
hook before_template_render => sub {
|
||||
my $tokens = shift;
|
||||
|
||||
$tokens->{uris}->{root} = uri_for("/");
|
||||
$tokens->{uris}->{login} = uri_for("/login");
|
||||
$tokens->{uris}->{logout} = uri_for("/logout");
|
||||
$tokens->{uris}->{random} = uri_for("/random");
|
||||
$tokens->{uris}->{tags} = uri_for("/tags");
|
||||
};
|
||||
|
||||
get "/" => sub {
|
||||
template "index" => {
|
||||
"title" => "main",
|
||||
};
|
||||
};
|
||||
|
||||
get "/tags" => sub {
|
||||
};
|
||||
|
||||
get "/tag/:tag_id" => sub {
|
||||
};
|
||||
|
||||
get "/random" => sub {
|
||||
};
|
||||
|
||||
get "/image/:image_id" => sub {
|
||||
};
|
||||
|
||||
true;
|
113
lib/PoorBooru/API/V0.pm
Normal file
113
lib/PoorBooru/API/V0.pm
Normal file
@ -0,0 +1,113 @@
|
||||
package PoorBooru::API::V0;
|
||||
use Dancer2;
|
||||
use Dancer2::Plugin::DBIC;
|
||||
|
||||
our $VERSION = v0;
|
||||
|
||||
set serializer => "JSON";
|
||||
set database => "db/booru.db";
|
||||
|
||||
my $DEFAULT_CONTENT_TYPE = "application/json";
|
||||
my @ROUTES = (
|
||||
{ path => "/meta", verb => "GET" },
|
||||
{ path => "/tags", verb => "GET" },
|
||||
{ path => "/tags/new", verb => "POST" },
|
||||
{ path => "/tag/:tag_id_or_name", verb => "GET" },
|
||||
{ path => "/random", verb => "GET" },
|
||||
{ path => "/media/:media_id", verb => "GET" },
|
||||
{ path => "/download/:media_id", verb => "GET" },
|
||||
);
|
||||
|
||||
get "/meta" => sub {
|
||||
return \@ROUTES;
|
||||
};
|
||||
|
||||
get "/tags" => sub {
|
||||
my @tags = schema("default")->resultset("TagsCountView")->all;
|
||||
|
||||
return [
|
||||
map +( {
|
||||
id => $_->tag_id,
|
||||
name => $_->name,
|
||||
count => $_->count,
|
||||
} ), @tags,
|
||||
];
|
||||
};
|
||||
|
||||
post "/tags/new" => sub {
|
||||
my $tag;
|
||||
|
||||
eval {
|
||||
$tag = schema("default")->resultset("Tag")
|
||||
->create({ name => body_parameters->get("name") });
|
||||
} or send_error("Tag exists", 409);
|
||||
|
||||
return {
|
||||
id => $tag->tag_id,
|
||||
name => $tag->name,
|
||||
};
|
||||
};
|
||||
|
||||
get "/tag/:tag_id_or_name" => sub {
|
||||
my ($tag, $tag_rset, @media);
|
||||
my $tag_id_or_name = route_parameters->get("tag_id_or_name");
|
||||
|
||||
$tag_rset = schema("default")->resultset("Tag");
|
||||
$tag = $tag_rset->single({ tag_id => $tag_id_or_name }) //
|
||||
$tag_rset->single({ name => $tag_id_or_name });
|
||||
|
||||
send_error("Tag not found", 404) if !defined($tag);
|
||||
|
||||
@media = map { $_->media_id } schema("default")->resultset("MediaTag")
|
||||
->search({ tag_id => $tag->tag_id })->all;
|
||||
|
||||
return {
|
||||
id => $tag->tag_id,
|
||||
name => $tag->name,
|
||||
media => [
|
||||
map +( {
|
||||
id => $_,
|
||||
uri => uri_for("/media/$_"),
|
||||
donwload_uri => uri_for("/download/$_"),
|
||||
} ), @media,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
get "/random" => sub {
|
||||
my $media = schema("default")->resultset("Media")
|
||||
->search({}, { order_by => \"random()", limit => 1 })->single;
|
||||
|
||||
send_error("Media not found", 404) if !defined($media);
|
||||
|
||||
forward "/media/" . $media->media_id;
|
||||
};
|
||||
|
||||
get "/media/:media_id" => sub {
|
||||
my $media = schema("default")->resultset("Media")
|
||||
->single({ media_id => route_parameters->get("media_id") });
|
||||
|
||||
send_error("Media not found", 404) if !defined($media);
|
||||
|
||||
return {
|
||||
id => $media->media_id,
|
||||
filename => $media->filename,
|
||||
size => length($media->content),
|
||||
download_uri => uri_for("/download/" . $media->media_id),
|
||||
};
|
||||
};
|
||||
|
||||
get "/download/:media_id" => sub {
|
||||
my $media = schema("default")->resultset("Media")
|
||||
->single({ media_id => route_parameters->get("media_id") });
|
||||
|
||||
send_error("Media not found", 404) if !defined($media);
|
||||
|
||||
send_file(
|
||||
\$media->content,
|
||||
content_type => $media->content_type // $DEFAULT_CONTENT_TYPE,
|
||||
filename => $media->filename,
|
||||
);
|
||||
};
|
||||
|
||||
true;
|
20
lib/PoorBooru/Schema.pm
Normal file
20
lib/PoorBooru/Schema.pm
Normal file
@ -0,0 +1,20 @@
|
||||
use utf8;
|
||||
package PoorBooru::Schema;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Schema';
|
||||
|
||||
__PACKAGE__->load_namespaces;
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:0PabNwBpp04P3y4a8O5MtA
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
108
lib/PoorBooru/Schema/Result/Media.pm
Normal file
108
lib/PoorBooru/Schema/Result/Media.pm
Normal file
@ -0,0 +1,108 @@
|
||||
use utf8;
|
||||
package PoorBooru::Schema::Result::Media;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
PoorBooru::Schema::Result::Media
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<media>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("media");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 media_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_auto_increment: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 content
|
||||
|
||||
data_type: 'blob'
|
||||
is_nullable: 0
|
||||
size: 10485760
|
||||
|
||||
=head2 filename
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=head2 content_type
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 1
|
||||
size: 255
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"media_id",
|
||||
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||
"content",
|
||||
{ data_type => "blob", is_nullable => 0, size => 10485760 },
|
||||
"filename",
|
||||
{ data_type => "text", is_nullable => 0, size => 255 },
|
||||
"content_type",
|
||||
{ data_type => "text", is_nullable => 1, size => 255 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</media_id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("media_id");
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 media_tags
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<PoorBooru::Schema::Result::MediaTag>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"media_tags",
|
||||
"PoorBooru::Schema::Result::MediaTag",
|
||||
{ "foreign.media_id" => "self.media_id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 tags
|
||||
|
||||
Type: many_to_many
|
||||
|
||||
Composing rels: L</media_tags> -> tag
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->many_to_many("tags", "media_tags", "tag");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:X0A6CSw6yWOYvxhMNRoS0g
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
99
lib/PoorBooru/Schema/Result/MediaTag.pm
Normal file
99
lib/PoorBooru/Schema/Result/MediaTag.pm
Normal file
@ -0,0 +1,99 @@
|
||||
use utf8;
|
||||
package PoorBooru::Schema::Result::MediaTag;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
PoorBooru::Schema::Result::MediaTag
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<media_tags>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("media_tags");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 media_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 tag_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"media_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
"tag_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</media_id>
|
||||
|
||||
=item * L</tag_id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("media_id", "tag_id");
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 media
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<PoorBooru::Schema::Result::Media>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"media",
|
||||
"PoorBooru::Schema::Result::Media",
|
||||
{ media_id => "media_id" },
|
||||
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
|
||||
);
|
||||
|
||||
=head2 tag
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<PoorBooru::Schema::Result::Tag>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"tag",
|
||||
"PoorBooru::Schema::Result::Tag",
|
||||
{ tag_id => "tag_id" },
|
||||
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CaXiI8+eaCoDbot368Hjyw
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
106
lib/PoorBooru/Schema/Result/Tag.pm
Normal file
106
lib/PoorBooru/Schema/Result/Tag.pm
Normal file
@ -0,0 +1,106 @@
|
||||
use utf8;
|
||||
package PoorBooru::Schema::Result::Tag;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
PoorBooru::Schema::Result::Tag
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<tags>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("tags");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 tag_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_auto_increment: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 name
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"tag_id",
|
||||
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||
"name",
|
||||
{ data_type => "text", is_nullable => 0, size => 255 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</tag_id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("tag_id");
|
||||
|
||||
=head1 UNIQUE CONSTRAINTS
|
||||
|
||||
=head2 C<name_unique>
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</name>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_unique_constraint("name_unique", ["name"]);
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 media_tags
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<PoorBooru::Schema::Result::MediaTag>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"media_tags",
|
||||
"PoorBooru::Schema::Result::MediaTag",
|
||||
{ "foreign.tag_id" => "self.tag_id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 medias
|
||||
|
||||
Type: many_to_many
|
||||
|
||||
Composing rels: L</media_tags> -> media
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->many_to_many("medias", "media_tags", "media");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tlbIFVg6S6LAWWiR0ioHFw
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
60
lib/PoorBooru/Schema/Result/TagsCountView.pm
Normal file
60
lib/PoorBooru/Schema/Result/TagsCountView.pm
Normal file
@ -0,0 +1,60 @@
|
||||
use utf8;
|
||||
package PoorBooru::Schema::Result::TagsCountView;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
PoorBooru::Schema::Result::TagsCountView
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
__PACKAGE__->table_class("DBIx::Class::ResultSource::View");
|
||||
|
||||
=head1 TABLE: C<tags_count_view>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("tags_count_view");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 tag_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_nullable: 1
|
||||
|
||||
=head2 name
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 1
|
||||
size: 255
|
||||
|
||||
=head2 count
|
||||
|
||||
data_type: (empty string)
|
||||
is_nullable: 1
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"tag_id",
|
||||
{ data_type => "integer", is_nullable => 1 },
|
||||
"name",
|
||||
{ data_type => "text", is_nullable => 1, size => 255 },
|
||||
"count",
|
||||
{ data_type => "", is_nullable => 1 },
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-02-18 09:09:31
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9NUzqweyXVu/G84fxb3eew
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
150
public/css/style.css
Normal file
150
public/css/style.css
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* PoorBooru - Poorman's booru
|
||||
*
|
||||
* Written in 2023 by Lucas
|
||||
*
|
||||
* To the extent possible under law, the author(s) have dedicated all
|
||||
* copyright and related and neighboring rights to this software to the
|
||||
* public domain worldwide. This software is distributed without any
|
||||
* warranty.
|
||||
*
|
||||
* You should have received a copy of the CC0 Public Domain Dedication
|
||||
* along with this software. If not, see
|
||||
* <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
*/
|
||||
/*
|
||||
* Some parts copied from https://piccalil.li/blog/a-modern-css-reset/
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg-color: #0c0700;
|
||||
--fg-color: #fff8f0;
|
||||
--accent-color-0: #ec57bc;
|
||||
--accent-color-1: #f7be00;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--fg-color);
|
||||
font-family: monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1,
|
||||
p {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
footer > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color-0);
|
||||
}
|
||||
|
||||
a:link,
|
||||
a:visited {
|
||||
color: var(--accent-color-0);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active {
|
||||
color: var(--accent-color-1);
|
||||
}
|
||||
|
||||
nav {
|
||||
font-size: 1.25rem;
|
||||
line-height: 2.4;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
margin: 0 auto;
|
||||
max-width: 60rem;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.border-accent {
|
||||
border-color: var(--accent-color-0);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.fg-color {
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.fg-accent {
|
||||
color: var(--accent-color-0);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link:link,
|
||||
.nav-link:visited {
|
||||
color: var(--accent-color-0);
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:active {
|
||||
color: var(--accent-color-1);
|
||||
}
|
||||
|
||||
.nav-link-gap {
|
||||
gap: 0 1rem;
|
||||
}
|
||||
|
||||
.flex-c-horizontal {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-c-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-c-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-i-fullsize {
|
||||
flex: auto;
|
||||
}
|
5
t/001_base.t
Normal file
5
t/001_base.t
Normal file
@ -0,0 +1,5 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Test::More tests => 1;
|
||||
use_ok 'PoorBooru';
|
16
t/002_index_route.t
Normal file
16
t/002_index_route.t
Normal file
@ -0,0 +1,16 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PoorBooru;
|
||||
use Test::More tests => 2;
|
||||
use Plack::Test;
|
||||
use HTTP::Request::Common;
|
||||
use Ref::Util qw<is_coderef>;
|
||||
|
||||
my $app = PoorBooru->to_app;
|
||||
ok( is_coderef($app), 'Got app' );
|
||||
|
||||
my $test = Plack::Test->create($app);
|
||||
my $res = $test->request( GET '/' );
|
||||
|
||||
ok( $res->is_success, '[GET /] successful' );
|
1
views/index.tt
Normal file
1
views/index.tt
Normal file
@ -0,0 +1 @@
|
||||
<h1>No content</h1>
|
27
views/layouts/main.tt
Normal file
27
views/layouts/main.tt
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="[% settings.charset %]">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<title>[% settings.appname %] - [% title %]</title>
|
||||
<link rel="stylesheet" href="[% request.uri_base %]/css/style.css">
|
||||
</head>
|
||||
<body class="bg-default fg-default flex-c-vertical">
|
||||
<header class="border-bottom border-accent">
|
||||
<nav class="viewport flex-c-horizontal nav-link-gap">
|
||||
<a class="nav-link" href="[% uris.root %]">[% settings.appname %]</a>
|
||||
<a class="nav-link" href="[% uris.random %]">random</a>
|
||||
<a class="nav-link" href="[% uris.tags %]">tags</a>
|
||||
<span class="flex-i-fullsize"><!-- spacer --></span>
|
||||
<a class="nav-link" href="[% uris.login %]">login</a>
|
||||
</nav>
|
||||
</header>
|
||||
<!--<main class="viewport flex-i-fullsize flex-c-vertical flex-c-center">-->
|
||||
<main class="viewport flex-i-fullsize">
|
||||
[% content -%]
|
||||
</main>
|
||||
<footer class="text-center">
|
||||
<p class="viewport">Powered by <a href="https://www.openbsd.org">OpenBSD</a> and <a href="https://perldancer.org/">Dancer2</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user