27 Commits

Author SHA1 Message Date
eai04191
cc905a0bce WIP: Change Okazu view 2019-08-13 21:25:37 +09:00
shibafu
f7c9e83b12 Merge pull request #251 from shikorism/schedule-ci
定期的に実際のリクエストを伴うMetadataResolverのテストを実行する
2019-08-10 12:36:31 +09:00
shibafu
a1850b666b 定期テストではMetadataResolverのテストのみを実行する 2019-08-10 12:18:36 +09:00
shibafu
dddb47f68a Merge pull request #248 from eai04191/feature/unknown-territory
Add XtubeResolver
2019-08-08 00:26:17 +09:00
shibafu
1b2b043be2 Merge pull request #246 from shikorism/feature/monthly-graph-term
月間チェックイングラフの表示期間を選択可能にする
2019-08-08 00:23:15 +09:00
shibafu
c535153e1f 定期的に実際のリクエストを伴うMetadataResolverのテストを実行する 2019-08-08 00:14:59 +09:00
eai04191
830de3a5e3 Add XtubeResolver 2019-08-06 22:44:38 +09:00
shibafu
ab46117138 対象年の表記を yyyy年 にした 2019-08-04 01:33:54 +09:00
shibafu
3c2fec21a0 月間チェックイングラフの対象年を切り替えられるようにした 2019-08-04 01:31:53 +09:00
shibafu
370d1cc01b 月間グラフのデータ整形をクライアントサイドでやらせる 2019-08-04 00:57:35 +09:00
shibafu
5517cd5fab Merge pull request #244 from shikorism/prestissimo
prestissimoのインストールをDockerfileに含める
2019-08-03 17:17:34 +09:00
shibafu
3c083a7c60 Merge pull request #243 from shikorism/fix/235-tora
ToranoanaResolverで、OGP画像の代わりになりそうなサムネ画像を取得する
2019-08-03 17:06:56 +09:00
shibafu
fa6b8b87af READMEからprestissimoのインストールを削除 2019-08-03 01:00:15 +09:00
shibafu
d290bf4107 prestissimoを予めインストールしておく 2019-08-03 00:59:53 +09:00
shibafu
b274c6bc40 Cookieを付与しなくてもデータを取得できたので、付与しないように変更 2019-08-03 00:26:12 +09:00
shibafu
018532f01f ToranoanaResolverのテストを追加 2019-08-03 00:16:21 +09:00
shibafu
e4ef935dd2 OGP画像が全て代替イメージになっていたため、サムネイルのスクレイピングを実施 2019-08-02 23:48:56 +09:00
shibafu
fb84a1d416 Merge pull request #241 from shikorism/dependabot/npm_and_yarn/lodash-4.17.15
Bump lodash from 4.17.11 to 4.17.15
2019-07-25 20:17:24 +09:00
dependabot[bot]
358580a15e Bump lodash from 4.17.11 to 4.17.15
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-25 11:12:45 +00:00
shibafu
000b89f380 Merge pull request #237 from eai04191/feature/card-round-fix
カードの角丸を修正
2019-07-14 01:06:51 +09:00
shibafu
c5cbad4475 Merge pull request #238 from shikorism/feature/disable-inserted-tag
入力済みのタグ候補の背景色を変更する
2019-07-14 01:06:02 +09:00
shibafu
f4abb08921 編集モードの初期表示時に入力済みタグが反映されていなかったので、現在のタグを配信させるイベントを定義 2019-07-04 22:34:28 +09:00
shibafu
38eb0348f9 入力済みのタグ候補の背景色を変更する 2019-07-04 22:33:28 +09:00
eai04191
4f5595dae0 vue側も追従 2019-07-04 21:28:54 +09:00
eai04191
733e97bc58 card-img類削除、.cardにoverflow付与 2019-07-04 21:28:26 +09:00
shibafu
c4768ded38 Revert "Merge pull request #138 from eai04191/feature/bootstrap-custom"
This reverts commit 077731495c.
2019-07-04 20:58:59 +09:00
eai04191
598d27f6b8 Revert "Merge pull request #153 from shikorism/fix/140"
This reverts commit d044b6db20.
2019-07-04 20:55:33 +09:00
32 changed files with 14039 additions and 67 deletions

View File

@@ -1,6 +1,6 @@
version: 2 version: 2.1
jobs: executors:
build: build:
docker: docker:
- image: circleci/php:7.1-node-browsers - image: circleci/php:7.1-node-browsers
@@ -17,38 +17,75 @@ jobs:
POSTGRES_DB: tissue POSTGRES_DB: tissue
POSTGRES_USER: tissue POSTGRES_USER: tissue
POSTGRES_PASSWORD: tissue POSTGRES_PASSWORD: tissue
commands:
initialize:
steps: steps:
- checkout - checkout
- run: sudo apt update - run: sudo apt update
- run: sudo apt install -y libpq-dev - run: sudo apt install -y libpq-dev
- run: sudo docker-php-ext-install zip - run: sudo docker-php-ext-install zip
- run: sudo docker-php-ext-install pdo_pgsql - run: sudo docker-php-ext-install pdo_pgsql
restore_composer:
steps:
- restore_cache: - restore_cache:
keys: keys:
- v1-dependencies-{{ checksum "composer.json" }} - v1-dependencies-{{ checksum "composer.json" }}
- v1-dependencies- - v1-dependencies-
- run: composer install -n --prefer-dist save_composer:
steps:
- save_cache: - save_cache:
key: v1-dependencies-{{ checksum "composer.json" }} key: v1-dependencies-{{ checksum "composer.json" }}
paths: paths:
- ./vendor - ./vendor
restore_npm:
steps:
- restore_cache: - restore_cache:
keys: keys:
- v1-dependencies-{{ checksum "package.json" }} - v1-dependencies-{{ checksum "package.json" }}
- v1-dependencies- - v1-dependencies-
- run: yarn install save_npm:
steps:
- save_cache: - save_cache:
key: v1-dependencies-{{ checksum "package.json" }} key: v1-dependencies-{{ checksum "package.json" }}
paths: paths:
- ./node_modules - ./node_modules
- ~/.yarn - ~/.yarn
- run: php artisan migrate jobs:
build:
executor: build
steps:
- initialize
- restore_composer
- run: composer install -n --prefer-dist
- save_composer
- restore_npm
- run: yarn install
- save_npm
- run: yarn run prod - run: yarn run prod
- persist_to_workspace:
root: .
paths:
- public
test:
executor: build
steps:
- initialize
- restore_composer
- restore_npm
- attach_workspace:
at: .
- run: php artisan migrate
# Run linter # Run linter
- run: - run:
command: | command: |
@@ -79,3 +116,51 @@ jobs:
- run: - run:
command: bash <(curl -s https://codecov.io/bash) -f /tmp/phpunit/coverage.xml command: bash <(curl -s https://codecov.io/bash) -f /tmp/phpunit/coverage.xml
when: always when: always
test_resolver:
executor: build
environment:
TEST_USE_HTTP_MOCK: false
steps:
- initialize
- restore_composer
- attach_workspace:
at: .
- run: php artisan migrate
# Run unit test
- run:
command: |
mkdir -p /tmp/phpunit
./vendor/bin/phpunit --testsuite MetadataResolver --log-junit /tmp/phpunit/phpunit.xml --coverage-clover=/tmp/phpunit/coverage.xml
when: always
- store_test_results:
path: /tmp/phpunit
- store_artifacts:
path: /tmp/phpunit/coverage.xml
workflows:
version: 2.1
test:
jobs:
- build
- test:
requires:
- build
scheduled_resolver_test:
triggers:
- schedule:
cron: "4 0 * * 1"
filters:
branches:
only:
- develop
jobs:
- build
- test_resolver:
requires:
- build

View File

@@ -10,6 +10,7 @@ RUN apt-get update \
&& pecl install xdebug \ && pecl install xdebug \
&& curl -sS https://getcomposer.org/installer | php \ && curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer \ && mv composer.phar /usr/local/bin/composer \
&& composer global require hirak/prestissimo \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \ && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& a2enmod rewrite && a2enmod rewrite

View File

@@ -36,7 +36,6 @@ docker-compose up -d
4. Composer と yarn を使い必要なライブラリをインストールします。 4. Composer と yarn を使い必要なライブラリをインストールします。
``` ```
docker-compose exec web composer global require hirak/prestissimo
docker-compose exec web composer install docker-compose exec web composer install
docker-compose exec web yarn install docker-compose exec web yarn install
``` ```

View File

@@ -109,13 +109,6 @@ SQL
} }
} }
// 月間グラフ用の配列初期化
$month = Carbon::now()->firstOfMonth()->subMonth(11); // 直近12ヶ月
for ($i = 0; $i < 12; $i++) {
$monthlySum[$month->format('Y/m')] = 0;
$month->addMonth();
}
foreach ($groupByDay as $data) { foreach ($groupByDay as $data) {
$date = Carbon::createFromFormat('Y/m/d', $data->date); $date = Carbon::createFromFormat('Y/m/d', $data->date);
$yearAndMonth = $date->format('Y/m'); $yearAndMonth = $date->format('Y/m');
@@ -123,21 +116,18 @@ SQL
$dailySum[$date->timestamp] = $data->count; $dailySum[$date->timestamp] = $data->count;
$yearlySum[$date->year] += $data->count; $yearlySum[$date->year] += $data->count;
$dowSum[$date->dayOfWeek] += $data->count; $dowSum[$date->dayOfWeek] += $data->count;
if (isset($monthlySum[$yearAndMonth])) { $monthlySum[$yearAndMonth] = ($monthlySum[$yearAndMonth] ?? 0) + $data->count;
$monthlySum[$yearAndMonth] += $data->count;
}
} }
foreach ($groupByHour as $data) { foreach ($groupByHour as $data) {
$hour = (int)$data->hour; $hour = (int)$data->hour;
$hourlySum[$hour] += $data->count; $hourlySum[$hour] += $data->count;
} }
$graphData = [ $graphData = [
'dailySum' => $dailySum, 'dailySum' => $dailySum,
'dowSum' => $dowSum, 'dowSum' => $dowSum,
'monthlyKey' => array_keys($monthlySum), 'monthlySum' => $monthlySum,
'monthlySum' => array_values($monthlySum),
'yearlyKey' => array_keys($yearlySum), 'yearlyKey' => array_keys($yearlySum),
'yearlySum' => array_values($yearlySum), 'yearlySum' => array_values($yearlySum),
'hourlyKey' => array_keys($hourlySum), 'hourlyKey' => array_keys($hourlySum),

View File

@@ -28,6 +28,7 @@ class MetadataResolver implements Resolver
'~www\.plurk\.com\/p\/.*~' => PlurkResolver::class, '~www\.plurk\.com\/p\/.*~' => PlurkResolver::class,
'~(adult\.)?contents\.fc2\.com\/article_search\.php\?id=\d+~' => FC2ContentsResolver::class, '~(adult\.)?contents\.fc2\.com\/article_search\.php\?id=\d+~' => FC2ContentsResolver::class,
'~store\.steampowered\.com/app/\d+~' => SteamResolver::class, '~store\.steampowered\.com/app/\d+~' => SteamResolver::class,
'~www\.xtube\.com/video-watch/.*-\d+$~'=> XtubeResolver::class,
]; ];
public $mimeTypes = [ public $mimeTypes = [

View File

@@ -24,11 +24,19 @@ class ToranoanaResolver implements Resolver
public function resolve(string $url): Metadata public function resolve(string $url): Metadata
{ {
$cookieJar = CookieJar::fromArray(['adflg' => '0'], 'ec.toranoana.jp'); $res = $this->client->get($url);
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) { if ($res->getStatusCode() === 200) {
return $this->ogpResolver->parse($res->getBody()); $metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$imgNode = $xpath->query('//*[@id="preview"]//img')->item(0);
if ($imgNode !== null) {
$metadata->image = $imgNode->getAttribute('src');
}
return $metadata;
} else { } else {
throw new \RuntimeException("{$res->getStatusCode()}: $url"); throw new \RuntimeException("{$res->getStatusCode()}: $url");
} }

View File

@@ -0,0 +1,41 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class XtubeResolver implements Resolver
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
if (preg_match('~www\.xtube\.com/video-watch/.*-(\d+)$~', $url, $matches) !== 1) {
throw new \RuntimeException("Unmatched URL Pattern: $url");
}
$videoid = $matches[1];
$res = $this->client->get('https://www.xtube.com/webmaster/api/getvideobyid?video_id=' . $videoid);
if ($res->getStatusCode() === 200) {
$data = json_decode($res->getBody()->getContents(), true);
$metadata = new Metadata();
$metadata->title = $data['title'] ?? '';
$metadata->description = strip_tags(str_replace('\n', PHP_EOL, html_entity_decode($data['description'] ?? '')));
$metadata->image = str_replace('eSuQ8f', 'eSK08f', $data['thumb'] ?? ''); // 300x169 to 300x210
$metadata->tags = array_values(array_unique($data['tags']));
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -16,6 +16,7 @@
"cal-heatmap": "^3.3.10", "cal-heatmap": "^3.3.10",
"chart.js": "^2.7.1", "chart.js": "^2.7.1",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"date-fns": "^1.30.1",
"husky": "^1.3.1", "husky": "^1.3.1",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",

View File

@@ -16,6 +16,10 @@
<testsuite name="Unit"> <testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory> <directory suffix="Test.php">./tests/Unit</directory>
</testsuite> </testsuite>
<testsuite name="MetadataResolver">
<directory suffix="Test.php">./tests/Unit/MetadataResolver</directory>
</testsuite>
</testsuites> </testsuites>
<filter> <filter>
<whitelist processUncoveredFilesFromWhitelist="true"> <whitelist processUncoveredFilesFromWhitelist="true">

View File

@@ -11,7 +11,7 @@
</div> </div>
<div v-else-if="state === MetadataLoadState.Success" class="row no-gutters"> <div v-else-if="state === MetadataLoadState.Success" class="row no-gutters">
<div v-if="hasImage" class="col-4 justify-content-center align-items-center"> <div v-if="hasImage" class="col-4 justify-content-center align-items-center">
<img :src="metadata.image" alt="Thumbnail" class="card-img-top-to-left bg-secondary"> <img :src="metadata.image" alt="Thumbnail" class="w-100 bg-secondary">
</div> </div>
<div :class="descClasses"> <div :class="descClasses">
<div class="card-body"> <div class="card-body">
@@ -20,8 +20,8 @@
<p class="card-text mb-2" style="font-size: small;">タグ候補<br><span class="text-secondary">(クリックするとタグ入力欄にコピーできます)</span></p> <p class="card-text mb-2" style="font-size: small;">タグ候補<br><span class="text-secondary">(クリックするとタグ入力欄にコピーできます)</span></p>
<ul class="list-inline d-inline"> <ul class="list-inline d-inline">
<li v-for="tag in suggestions" <li v-for="tag in suggestions"
class="list-inline-item badge badge-primary metadata-tag-item" :class="tagClasses(tag)"
@click="addTag(tag)"><span class="oi oi-tag"></span> {{ tag }}</li> @click="addTag(tag.name)"><span class="oi oi-tag"></span> {{ tag.name }}</li>
</ul> </ul>
</template> </template>
</div> </div>
@@ -54,6 +54,11 @@
}[], }[],
}; };
type Suggestion = {
name: string,
used: boolean,
}
@Component @Component
export default class MetadataPreview extends Vue { export default class MetadataPreview extends Vue {
@Prop() readonly state!: MetadataLoadState; @Prop() readonly state!: MetadataLoadState;
@@ -62,16 +67,38 @@
// for use in v-if // for use in v-if
private readonly MetadataLoadState = MetadataLoadState; private readonly MetadataLoadState = MetadataLoadState;
tags: string[] = [];
created() {
bus.$on("change-tag", (tags: string[]) => this.tags = tags);
bus.$emit("resend-tag");
}
addTag(tag: string) { addTag(tag: string) {
bus.$emit("add-tag", tag); bus.$emit("add-tag", tag);
} }
get suggestions() { tagClasses(s: Suggestion) {
return {
"list-inline-item": true,
"badge": true,
"badge-primary": !s.used,
"badge-secondary": s.used,
"metadata-tag-item": true,
};
}
get suggestions(): Suggestion[] {
if (this.metadata === null) { if (this.metadata === null) {
return []; return [];
} }
return this.metadata.tags.map(t => t.name); return this.metadata.tags.map(t => {
return {
name: t.name,
used: this.tags.indexOf(t.name) !== -1
};
});
} }
get hasImage() { get hasImage() {
@@ -90,10 +117,7 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.link-card-mini { .link-card-mini {
$height: 150px; $height: 150px;
overflow: hidden;
.row > div {
overflow: hidden;
}
.row > div:first-child { .row > div:first-child {
display: flex; display: flex;

View File

@@ -16,7 +16,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {Vue, Component, Prop} from "vue-property-decorator"; import {Vue, Component, Prop, Watch} from "vue-property-decorator";
import {bus} from "../checkin"; import {bus} from "../checkin";
@Component @Component
@@ -31,6 +31,7 @@
created() { created() {
bus.$on("add-tag", (tag: string) => this.tags.indexOf(tag) === -1 && this.tags.push(tag)); bus.$on("add-tag", (tag: string) => this.tags.indexOf(tag) === -1 && this.tags.push(tag));
bus.$on("resend-tag", () => bus.$emit("change-tag", this.tags));
} }
onKeyDown(event: KeyboardEvent) { onKeyDown(event: KeyboardEvent) {
@@ -56,6 +57,11 @@
this.tags.splice(index, 1); this.tags.splice(index, 1);
} }
@Watch("tags")
onTagsChanged() {
bus.$emit("change-tag", this.tags);
}
get containerClass(): object { get containerClass(): object {
return { return {
"form-control": true, "form-control": true,

View File

@@ -1,9 +1,12 @@
import CalHeatMap from 'cal-heatmap'; import CalHeatMap from 'cal-heatmap';
import Chart from 'chart.js'; import Chart from 'chart.js';
import {addMonths, format, startOfMonth, subMonths} from 'date-fns';
const graphData = JSON.parse(document.getElementById('graph-data').textContent);
function createLineGraph(id, labels, data) { function createLineGraph(id, labels, data) {
const context = document.getElementById(id).getContext('2d'); const context = document.getElementById(id).getContext('2d');
new Chart(context, { return new Chart(context, {
type: 'line', type: 'line',
data: { data: {
labels: labels, labels: labels,
@@ -62,7 +65,22 @@ function createBarGraph(id, labels, data) {
}); });
} }
const graphData = JSON.parse(document.getElementById('graph-data').textContent); /**
* @param {Date} from
*/
function createMonthlyGraphData(from) {
const keys = [];
const values = [];
for (let i = 0; i < 12; i++) {
const current = addMonths(from, i);
const yearAndMonth = format(current, 'YYYY/MM');
keys.push(yearAndMonth);
values.push(graphData.monthlySum[yearAndMonth] || 0);
}
return {keys, values};
}
new CalHeatMap().init({ new CalHeatMap().init({
itemSelector: '#cal-heatmap', itemSelector: '#cal-heatmap',
@@ -76,7 +94,40 @@ new CalHeatMap().init({
legend: [1, 2, 3, 4] legend: [1, 2, 3, 4]
}); });
createLineGraph('monthly-graph', graphData.monthlyKey, graphData.monthlySum); // 直近1年の月間グラフのデータを準備
const monthlyTermFrom = subMonths(startOfMonth(new Date()), 11);
const {keys: monthlyKey, values: monthlySum} = createMonthlyGraphData(monthlyTermFrom);
const monthlyGraph = createLineGraph('monthly-graph', monthlyKey, monthlySum);
createLineGraph('yearly-graph', graphData.yearlyKey, graphData.yearlySum); createLineGraph('yearly-graph', graphData.yearlyKey, graphData.yearlySum);
createBarGraph('hourly-graph', graphData.hourlyKey, graphData.hourlySum); createBarGraph('hourly-graph', graphData.hourlyKey, graphData.hourlySum);
createBarGraph('dow-graph', ['日', '月', '火', '水', '木', '金', '土'], graphData.dowSum); createBarGraph('dow-graph', ['日', '月', '火', '水', '木', '金', '土'], graphData.dowSum);
// 月間グラフの期間セレクターを準備
const monthlyTermSelector = document.getElementById('monthly-term');
for (let year = monthlyTermFrom.getFullYear(); year <= new Date().getFullYear(); year++) {
const opt = document.createElement('option');
opt.setAttribute('value', year);
opt.textContent = `${year}`;
monthlyTermSelector.insertBefore(opt, monthlyTermSelector.firstChild);
}
if (monthlyTermSelector.children.length) {
monthlyTermSelector.selectedIndex = 0;
}
monthlyTermSelector.addEventListener('change', function (e) {
let monthlyTermFrom;
if (e.target.selectedIndex === 0) {
// 今年のデータを表示する時は、直近12ヶ月を表示
monthlyTermFrom = subMonths(startOfMonth(new Date()), 11);
} else {
// 過去のデータを表示する時は、選択年の1〜12月を表示
monthlyTermFrom = new Date(e.target.value, 0, 1);
}
const {keys, values} = createMonthlyGraphData(monthlyTermFrom);
monthlyGraph.data.labels = keys;
monthlyGraph.data.datasets[0].data = values;
monthlyGraph.update();
});

View File

@@ -1,19 +0,0 @@
.card-img-left {
width: 100%;
@include border-left-radius($card-inner-border-radius);
}
.card-img-right {
width: 100%;
@include border-right-radius($card-inner-border-radius);
}
.card-img-top-to-left {
width: 100%;
@include media-breakpoint-down(md) {
@include border-top-radius($card-inner-border-radius);
}
@include media-breakpoint-up(lg) {
@include border-left-radius($card-inner-border-radius);
}
}

View File

@@ -3,7 +3,6 @@ $primary: #e53fb1;
// Bootstrap // Bootstrap
@import "~bootstrap/scss/bootstrap"; @import "~bootstrap/scss/bootstrap";
@import "bootstrap-custom";
// Open Iconic // Open Iconic
@import "~open-iconic/font/css/open-iconic-bootstrap"; @import "~open-iconic/font/css/open-iconic-bootstrap";

View File

@@ -1,4 +1,6 @@
.link-card { .link-card {
overflow: hidden;
.row > div { .row > div {
max-height: 400px; max-height: 400px;
overflow: hidden; overflow: hidden;

View File

@@ -76,4 +76,5 @@
#navbarAccountDropdownSp { #navbarAccountDropdownSp {
max-width: calc(100vw - 5em); max-width: calc(100vw - 5em);
} }

View File

@@ -2,7 +2,7 @@
<a class="text-dark card-link" href="{{ $link }}" target="_blank" rel="noopener"> <a class="text-dark card-link" href="{{ $link }}" target="_blank" rel="noopener">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-12 col-md-6 justify-content-center align-items-center"> <div class="col-12 col-md-6 justify-content-center align-items-center">
<img src="" alt="Thumbnail" class="card-img-top-to-left bg-secondary"> <img src="" alt="Thumbnail" class="w-100 bg-secondary">
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="card-body"> <div class="card-body">

View File

@@ -0,0 +1,9 @@
<div class="card okazu-card overflow-hidden d-none">
<a href="{{ $link }}" target="_blank" rel="noopener">
<img src="" class="card-img" alt="">
<div class="card-img-overlay d-flex align-items-end p-3">
<h5 class="card-title overflow-hidden m-0 font-weight-bold p-1" style="text-overflow: ellipsis;white-space: nowrap;background: rgba(229, 63, 177, 0.75);color: white;">Card title</h5>
<p class="card-text overflow-hidden d-none" style="text-overflow: ellipsis;white-space: nowrap;">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
</div>
</a>
</div>

View File

@@ -31,6 +31,41 @@
<p class="mt-4"> <p class="mt-4">
<span class="oi oi-lock-locked"></span> このユーザはチェックイン履歴を公開していません。 <span class="oi oi-lock-locked"></span> このユーザはチェックイン履歴を公開していません。
</p> </p>
@elseif (Route::currentRouteName() === 'user.okazu')
@push('script')
<script>
document.querySelectorAll(".okazu-card").forEach(card => {
const title = card.querySelector(".card-title");
const description = card.querySelector(".card-text");
const image = card.querySelector(".card-img");
const params = new URLSearchParams();
params.set('url', card.querySelector("a").href);
fetch("/api/checkin/card?" + params.toString())
.then(response => response.json())
.then(json => {
if (json.image) {
title.innerText = json.title;
description.innerText = json.description;
image.setAttribute("src", json.image);
card.classList.remove("d-none");
}
})
});
</script>
@endpush
<div class="card-columns mt-3">
@forelse ($ejaculations as $ejaculation)
@component('components.okazu-card', ['link' => $ejaculation->link])
@endcomponent
@empty
<li class="list-group-item border-bottom-only">
<p>まだチェックインしていません。</p>
</li>
@endforelse
</div>
@else @else
<ul class="list-group"> <ul class="list-group">
@forelse ($ejaculations as $ejaculation) @forelse ($ejaculations as $ejaculation)

View File

@@ -15,7 +15,14 @@
<h5 class="my-4">Shikontribution graph</h5> <h5 class="my-4">Shikontribution graph</h5>
<div id="cal-heatmap" class="tis-contribution-graph"></div> <div id="cal-heatmap" class="tis-contribution-graph"></div>
<hr class="my-4"> <hr class="my-4">
<h5 class="my-4">月間チェックイン回数</h5> <div class="row my-4">
<div class="col-12 col-lg-6 d-flex align-items-center">
<h5 class="my-0">月間チェックイン回数</h5>
</div>
<div class="col-12 col-lg-6 mt-2 mt-lg-0">
<select id="monthly-term" class="form-control"></select>
</div>
</div>
<canvas id="monthly-graph" class="w-100"></canvas> <canvas id="monthly-graph" class="w-100"></canvas>
<hr class="my-4"> <hr class="my-4">
<h5 class="my-4">年間チェックイン回数</h5> <h5 class="my-4">年間チェックイン回数</h5>

View File

@@ -0,0 +1,140 @@
<?php
namespace Tests\Unit\MetadataResolver;
use App\MetadataResolver\ToranoanaResolver;
use Tests\TestCase;
class ToranoanaResolverTest extends TestCase
{
use CreateMockedResolver;
public function setUp()
{
parent::setUp();
if (!$this->shouldUseMock()) {
sleep(1);
}
}
public function testTora()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testTora.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.shop/tora/ec/item/040030720152');
$this->assertEquals('新・古明地喫茶~そしてまた扉は開く~', $metadata->title);
$this->assertEquals('サークル【ツキギのとこ】(槻木こうすけ)発行の「新・古明地喫茶~そしてまた扉は開く~」を買うなら、とらのあな全年齢向け通信販売!', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.shop/tora/ec/item/040030720152', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testToraR()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testToraR.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.jp/tora_r/ec/item/040030720174');
$this->assertEquals('お姉ちゃんが妹のぱんつでひとりえっちしてました。', $metadata->title);
$this->assertEquals('サークル【没後】RYO発行の「お姉ちゃんが妹のぱんつでひとりえっちしてました。」を買うなら、とらのあな成年向け通信販売', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.jp/tora_r/ec/item/040030720174', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testToraD()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testToraD.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.shop/tora_d/digi/item/042000013358');
$this->assertEquals('虎の穴ラボの薄い本。vol 1.5', $metadata->title);
$this->assertEquals('サークル【虎の穴ラボ】虎の穴ラボエンジニアチーム発行の「虎の穴ラボの薄い本。vol 1.5」を買うなら、とらのあな全年齢向け電子書籍!', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.shop/tora_d/digi/item/042000013358', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testToraRD()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testToraRD.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.jp/tora_rd/digi/item/042000013181');
$this->assertEquals('放課後のお花摘み', $metadata->title);
$this->assertEquals('サークル【給食泥棒】(村雲)発行の「放課後のお花摘み」を買うなら、とらのあな成年向け電子書籍!', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.jp/tora_rd/digi/item/042000013181', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testJoshi()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testJoshi.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.shop/joshi/ec/item/040030702729');
$this->assertEquals('円卓のクソ漫画', $metadata->title);
$this->assertEquals('サークル【地獄のすなぎもカーニバル】槌田発行の「円卓のクソ漫画」を買うなら、とらのあなJOSHIBU全年齢向け通信販売', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.shop/joshi/ec/item/040030702729', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testJoshiR()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testJoshiR.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.jp/joshi_r/ec/item/040030730126');
$this->assertEquals('リバースナイトリバース', $metadata->title);
$this->assertEquals('サークル【雨傘サイクル】チャリリズム発行の「リバースナイトリバース」を買うなら、とらのあなJOSHIBU成年向け通信販売', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.jp/joshi_r/ec/item/040030730126', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testJoshiD()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testJoshiD.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.shop/joshi_d/digi/item/042000012192');
$this->assertEquals('超幸運ガール審神者GOLDEN', $metadata->title);
$this->assertEquals('サークル【Day Of The Dead】ほんちゅ発行の「超幸運ガール審神者GOLDEN」を買うなら、とらのあなJOSHIBU全年齢向け電子書籍', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.shop/joshi_d/digi/item/042000012192', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testJoshiRD()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Toranoana/testJoshiRD.html');
$this->createResolver(ToranoanaResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://ec.toranoana.jp/joshi_rd/digi/item/042000013472');
$this->assertEquals('UBWの裏側で非公式に遠坂凛をナデナデする本', $metadata->title);
$this->assertEquals('サークル【阿仁谷組】阿仁谷ユイジ発行の「UBWの裏側で非公式に遠坂凛をナデナデする本」を買うなら、とらのあなJOSHIBU成年向け電子書籍', $metadata->description);
$this->assertRegExp('~ecdnimg\.toranoana\.jp/ec/img/.*\.jpg~', $metadata->image);
if ($this->shouldUseMock()) {
$this->assertSame('https://ec.toranoana.jp/joshi_rd/digi/item/042000013472', (string) $this->handler->getLastRequest()->getUri());
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\Unit\MetadataResolver;
use App\MetadataResolver\XtubeResolver;
use Tests\TestCase;
class XtubeResolverTest extends TestCase
{
use CreateMockedResolver;
public function setUp()
{
parent::setUp();
if (!$this->shouldUseMock()) {
sleep(1);
}
}
public function test()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/Xtube/test.json');
$this->createResolver(XtubeResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://www.xtube.com/video-watch/homegrown-big-tits-18634762');
$this->assertEquals('Homegrown Big Tits', $metadata->title);
$this->assertEquals('Dedicated to the fans of the beautiful amateur women with big natural tits. All user submitted - you can see big boob amateur hotties fucking and sucking as their tits bounce and sway.', $metadata->description);
$this->assertRegExp('~https://cdn\d-s-hw-e5\.xtube\.com/m=eSK08f/videos/201302/07/RF4Nk-S774-/240X180/1\.jpg~', $metadata->image);
$this->assertEquals(['bigtits', 'homeg'], $metadata->tags);
}
public function testNotMatch()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Unmatched URL Pattern: https://www.xtube.com/gallery/black-celebs-free-7686657');
$this->createResolver(XtubeResolver::class, '');
$this->resolver->resolve('https://www.xtube.com/gallery/black-celebs-free-7686657');
}
public function testNotOK()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('404: https://www.xtube.com/video-watch/notfound-404');
$this->createResolver(XtubeResolver::class, '', [], 404);
$this->resolver->resolve('https://www.xtube.com/video-watch/notfound-404');
}
}

1675
tests/fixture/Toranoana/testJoshi.html vendored Normal file

File diff suppressed because it is too large Load Diff

1588
tests/fixture/Toranoana/testJoshiD.html vendored Normal file

File diff suppressed because it is too large Load Diff

1738
tests/fixture/Toranoana/testJoshiR.html vendored Normal file

File diff suppressed because it is too large Load Diff

1696
tests/fixture/Toranoana/testJoshiRD.html vendored Normal file

File diff suppressed because it is too large Load Diff

1794
tests/fixture/Toranoana/testTora.html vendored Normal file

File diff suppressed because it is too large Load Diff

1674
tests/fixture/Toranoana/testToraD.html vendored Normal file

File diff suppressed because it is too large Load Diff

1744
tests/fixture/Toranoana/testToraR.html vendored Normal file

File diff suppressed because it is too large Load Diff

1626
tests/fixture/Toranoana/testToraRD.html vendored Normal file

File diff suppressed because it is too large Load Diff

1
tests/fixture/Xtube/test.json vendored Normal file
View File

@@ -0,0 +1 @@
{"duration":"180","views":3146,"video_id":"RF4Nk-S774-","rating":"4.000","ratings":"1","title":"Homegrown Big Tits","description":"Dedicated to the fans of the beautiful amateur women with big natural tits. All user submitted - you can see big boob amateur hotties fucking and sucking as their tits bounce and sway.","url":"https:\/\/www.xtube.com\/video-watch\/homegrown-big-tits-18634762","embedCode":"https:\/\/www.xtube.com\/video-watch\/embedded\/homegrown-big-tits-18634762","default_thumb":"https:\/\/cdn5-s-hw-e5.xtube.com\/m=eSuQ8f\/videos\/201302\/07\/RF4Nk-S774-\/240X180\/1.jpg","thumb":"https:\/\/cdn5-s-hw-e5.xtube.com\/m=eSuQ8f\/videos\/201302\/07\/RF4Nk-S774-\/240X180\/1.jpg","publish_date":"2013-02-07 17:41:10","tags":{"1396":"bigtits","472012":"homeg"},"thumbs":[{"width":300,"height":210,"src":"https:\/\/cdn4-s-hw-e5.xtube.com\/m=eSK08f\/videos\/201302\/07\/RF4Nk-S774-\/240X180\/1.jpg"},{"width":300,"height":210,"src":"https:\/\/cdn4-s-hw-e5.xtube.com\/m=eSK08f\/videos\/201302\/07\/RF4Nk-S774-\/240X180\/2.jpg"},{"width":300,"height":210,"src":"https:\/\/cdn10-s-hw-e5.xtube.com\/m=eSK08f\/videos\/201302\/07\/RF4Nk-S774-\/240X180\/3.jpg"}]}

View File

@@ -2333,7 +2333,7 @@ d3@^3.0.6:
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g= integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=
date-fns@^1.27.2: date-fns@^1.27.2, date-fns@^1.30.1:
version "1.30.1" version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
@@ -4607,9 +4607,9 @@ lodash.uniq@^4.5.0:
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5: lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5:
version "4.17.11" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
log-symbols@^1.0.2: log-symbols@^1.0.2:
version "1.0.2" version "1.0.2"