198 Commits

Author SHA1 Message Date
eai04191
f4860de95b デモ 2019-02-25 01:57:22 +09:00
eai04191
dc98334a6d cardをコンポーネント化 2019-02-23 22:29:53 +09:00
shibafu
32b0b76032 Merge pull request #104 from hinaloe/test/fix-crlf
テスト用ファイルの改行コードなおした
2019-02-18 20:14:31 +09:00
hina
db3ba04091 立つ鳥跡を濁さず
モックを使用しない場合のsleepを復活

close #102
2019-02-18 17:33:52 +09:00
hina
0400bc771c gitに改行コード変えられてた 2019-02-18 17:32:09 +09:00
shibafu
a5b4eeee36 Merge pull request #99 from shikorism/fix/68-paginator
ページャーの改善
2019-02-17 20:41:46 +09:00
shibafu
f760ea7093 Merge pull request #101 from hinaloe/tests/mockable-resolver
GuzzleHttpをモック可能にする
2019-02-17 19:36:56 +09:00
shibafu
0f4dfcd816 全ページ数にはレイアウト調整犠牲になってもらう
* .page-linkと同様にline-heightを設定
* 高さ調整のためにheightを設定
2019-02-17 11:13:12 +09:00
hina
a934a7fc35 リモートURLをリクエストするテストをトグれるモックに差し替える 2019-02-17 03:19:27 +09:00
hina
51f097fdf0 最初にやったニジエに抜けがあった 2019-02-17 03:18:59 +09:00
hina
24dee801ad Guzzle\Clientをモッカブルにする 2019-02-17 02:58:36 +09:00
hina
9f1cd607d7 ignore ide-helper files 2019-02-17 02:51:52 +09:00
hina
4196b1a02d Add barryvdh/laravel-ide-helper 2019-02-17 02:51:27 +09:00
Eai
35789befc5 pageが反映されていない問題を修正 (#98) 2019-02-17 00:24:25 +09:00
shibafu
32139cb9da ページネーションの自前HTMLを排除 2019-02-16 23:26:09 +09:00
shibafu
9244b8424d 最終ページを出してみる 2019-02-16 23:06:58 +09:00
shibafu
55eb95dda8 スマホ向けページャーでページジャンプを可能にした 2019-02-16 23:03:21 +09:00
shibafu
72e9d4e3e8 スマートフォン向けの簡易なページャーを用意 2019-02-16 23:03:19 +09:00
shibafu
852f1ac88c Bootstrap4用のページネーションテンプレートをpublish 2019-02-16 20:08:28 +09:00
shibafu
33be0ac8ef CienResolverを追加 (#95) 2019-02-16 00:06:51 +09:00
shibafu
9f2e73e511 トップページに表示されるグラフが少なすぎるバグの修正 (#94) 2019-02-12 23:46:20 +09:00
shibafu
09bb98876c Merge pull request #80 from shikorism/feature/noscript-info
ブラウザ設定でJavaScriptが無効になっている場合、メッセージを表示
2019-02-12 22:55:45 +09:00
Eai
cedee0a20e pixivのOGP画像で常にプロキシURLを使用する (#92) 2019-02-12 22:48:44 +09:00
unarist
116dd3b798 OGPResolverでmeta[name="description"] にフォールバックする🐯 (#91) 2019-02-12 22:45:51 +09:00
unarist
735bb00eba 著作権表示の年を更新 (#90) 2019-02-11 23:44:07 +09:00
shibafu
decb1707f1 PHP 7.0はオワコン 2019-02-11 13:18:27 +09:00
shibafu
bec7bdeb36 Merge pull request #89 from shikorism/feature/home-v2
ホーム画面の微リニューアル
2019-02-11 13:17:34 +09:00
shibafu
7f5a4a06d9 Merge pull request #86 from unarist/feat/activitypub
ActivityPubResolverを追加
2019-02-11 13:16:29 +09:00
unarist
d7c7f86ba5 CWもdescriptionに入れる 2019-02-11 03:07:52 +09:00
unarist
ca212b547a Acceptに*/*を入れると無視されるので入れないようにする 2019-02-11 03:07:52 +09:00
unarist
3584625b47 Actorの取得失敗をちゃんと無視するように 2019-02-11 03:07:52 +09:00
unarist
1ba4999a83 不明なContentTypeや406はOGPにフォールバックする 2019-02-11 03:07:52 +09:00
shibafu
03e1c2d60c カード情報のリクエスト処理を共通化 2019-02-10 20:48:16 +09:00
shibafu
ab0695ee8d xdebug.remote_autostartを使うと、デバッガーが不要な場合の処理時間が長くなりすぎるので止める 2019-02-10 18:58:58 +09:00
shibafu
fcafc3c704 レビュー内容を部分的に反映
* Bootstrapのスタイルに乗せた
* 問題解決の方法を案内
* fixedに関わりたくなくなってきたので単にページ先頭要素にした
2019-02-10 17:00:35 +09:00
shibafu
7a606be3ba 別にIIFEである必要性ないじゃん 2019-02-09 23:38:53 +09:00
shibafu
b4a7ec64dd デザインの微調整 2019-02-09 23:16:03 +09:00
unarist
5750eeb3a5 ActivityPubResolverを追加 2019-02-09 04:04:41 +09:00
shibafu
b4dc07a9a3 サーバ内全体の日別総チェックイン数グラフをトップに設置 2019-02-09 02:34:53 +09:00
shibafu
a77ac3f039 お惣菜コーナーのカード枠を削除
ユーザータイムラインに合わせた形
2019-02-09 00:50:47 +09:00
shibafu
eef0eac887 サイトからのお知らせを左カラムに追いやった 2019-02-09 00:17:12 +09:00
Eai
7337f60491 お知らせがpinnedの場合ピン留めの表示をする (#58) 2019-02-08 23:45:25 +09:00
shibafu
85e9599654 Merge pull request #85 from shikorism/feature/profile
プロフィール設定機能の強化 (自己紹介文, URL)
2019-02-07 21:36:04 +09:00
shibafu
8a919ca62a 新規登録時に、Gravatarの利用についての案内を表示 (#84) 2019-02-07 21:35:09 +09:00
shibafu
d105568c76 検索画面のページネーションURLに検索クエリが含まれるよう修正 (#83)
refs #82
2019-02-07 21:34:57 +09:00
shibafu
1f7723614d ホームで表示する自身の情報と、ユーザーページで表示するプロフィール情報を別のものにした 2019-02-07 00:52:44 +09:00
shibafu
41e810c788 プロフィール設定画面に自己紹介とURLを追加 2019-02-07 00:52:44 +09:00
shibafu
82af423c57 プロフィール欄に自己紹介とURLを掲載 2019-02-07 00:52:44 +09:00
shibafu
4346e1a701 usersテーブルに自己紹介とURLの列を追加 2019-02-07 00:52:44 +09:00
Eai
96199c9e46 年齢確認の画面でその他リンクをクリックできないように修正 (#81) 2019-02-05 22:33:11 +09:00
shibafu
4962244969 Merge pull request #77 from shikorism/feature/narou-resolver
NarouResolverを追加
2019-02-05 22:31:09 +09:00
unarist
c417dabff2 Pixivのmode=mangaに対応 (#79) 2019-02-04 02:43:40 +09:00
shibafu
d9bf673d85 作者名とあらすじの取得を試みる 2019-02-04 02:35:50 +09:00
shibafu
5961d3e27a ブラウザ設定でJavaScriptが無効になっている場合、メッセージを表示 2019-02-04 00:13:49 +09:00
unarist
b57a272611 非ログイン時は年齢確認を表示するように (#78) 2019-02-04 00:10:14 +09:00
shibafu
cbbb2605dd Merge pull request #38 from eai04191/feature/profile-card
アイコンの横に名前が来るようにする
2019-02-03 21:58:01 +09:00
Eai
e20bb75e00 PatreonResolverを追加 (#51) 2019-02-03 20:35:59 +09:00
eai04191
e226f43265 home でのみ省略表示するようにした 2019-02-03 16:47:16 +09:00
shibafu
e887f2d83e NarouResolverを追加
refs #70
2019-02-03 01:10:29 +09:00
shibafu
1835776a9c ログイン済の状態でログインページ等にアクセスした際のリダイレクト先が間違っていたので修正 (#75)
refs #69
2019-01-31 00:45:16 +09:00
shibafu
57715b9a82 タグ入力欄の高さが自動で拡張されるよう修正 (#74)
refs #71
2019-01-31 00:35:33 +09:00
shibafu
e320f85c73 月間グラフの表示範囲が月末になると1ヶ月ズレるバグの修正 (#73)
refs #72
2019-01-31 00:14:26 +09:00
shibafu
4ab82ff0e2 Revert "Merge pull request #30 from eai04191/feature-KomifloResolver"
This reverts commit b6bf1f99d8, reversing
changes made to ef563f8641.
2019-01-23 00:42:41 +09:00
shibafu
3c6f802b69 Merge pull request #41 from eai04191/feature/link-icon-flexbox
リンクアイコンの親要素をflexboxにしてリンクがアイコンから落ちないようにする
2019-01-23 00:21:32 +09:00
shibafu
895e9f4b15 OGPからタイトル情報が取れない場合はtitleタグから取得を試みる (#65)
refs #62
2019-01-23 00:04:10 +09:00
shibafu
648e171a57 Shift_JIS, EUC-JPでエンコードされたページのOGP取得対応 (#64)
refs #61
2019-01-22 12:46:44 +09:00
shibafu
dc91180dd4 チェックイン要素の全体にword-wrapを設定 (#63)
refs #28
2019-01-22 12:46:25 +09:00
Eai
bbbffcb39e DeviantArtResolverを追加 (#52) 2019-01-19 16:19:34 +09:00
shibafu
56831c78c3 Reformat time! 2019-01-19 03:02:37 +09:00
shibafu
a30919991c "composer fix" の追加 2019-01-19 03:02:30 +09:00
shibafu
8aa2e6a779 Merge pull request #60 from shikorism/fix/56-broken-normalize 2019-01-19 02:27:07 +09:00
shibafu
34f45d1ce8 Merge pull request #59 from shikorism/feature/settings
ユーザー設定画面 (プロフィール, プライバシー関連)
2019-01-19 02:26:35 +09:00
shibafu
6d66425fc9 正規化対象のURLにクエリパラメータとURLフラグメントが両方とも含まれる場合、正規化時に順序が崩れて不正なURLになってしまうバグの修正
refs #56
2019-01-19 02:18:35 +09:00
shibafu
626c85c07d URL正規化のテストを追加 2019-01-19 02:18:35 +09:00
shibafu
b6bf1f99d8 Merge pull request #30 from eai04191/feature-KomifloResolver
KomifloResolverに画像の取得を追加
2019-01-19 02:05:54 +09:00
eai04191
907eb87723 UTCのexpires_atをappのタイムゾーンに変換する
https://github.com/shikorism/tissue/pull/30#issuecomment-455174326
2019-01-19 01:41:11 +09:00
eai04191
72ec5d8d26 expires_atをCarbon化 2019-01-19 01:41:11 +09:00
eai04191
ca5be696c8 Komifloのメタデータに有効期限を設定する 2019-01-19 01:40:38 +09:00
eai04191
48ddac8c85 KomifloResolverに画像の取得を追加 2019-01-19 01:40:38 +09:00
shibafu
a2580f29cc プロフィール設定とプライバシー設定を分割 2019-01-19 01:17:56 +09:00
shibafu
85cc865545 プロフィール設定の実装 2019-01-19 00:49:58 +09:00
shibafu
5d256519c6 プロフィール設定画面のページタイトルを追加 2019-01-19 00:49:58 +09:00
shibafu
53b459740f プライバシー設定の更新を実装 2019-01-19 00:49:58 +09:00
shibafu
550d897561 明日やろうは馬鹿やろう?知らねぇなぁ! 2019-01-19 00:49:53 +09:00
shibafu
27532685ba POST先ルート追加 2019-01-19 00:12:10 +09:00
shibafu
4654962aac プロフィール設定ページを作ろう 2019-01-19 00:12:10 +09:00
Eai
ef563f8641 鍵垢だと概況データが出てこない問題を修正 (#53)
ref #50
2019-01-18 20:21:29 +09:00
Eai
7745b68dae php-cs-fixer のルールに single_quote を追加 (#54) 2019-01-18 20:00:47 +09:00
Eai
e5ea0528a8 docker-compose.debug.ymlを追加 (#55)
* docker-compose.debug.ymlを追加

* README追記
2019-01-18 20:00:24 +09:00
Eai
4e1eec66be FanzaResolver追加 (#43)
FanzaのOGP画像は `ps.jpg` か `pr.jpg` で終わっている。 `pl.jpg` で高解像度なものが取得できる。
2019-01-18 00:16:02 +09:00
shibafu
a33a0e542c Fix NijieResolverText (#42)
とりあえず通るようにしただけ
2019-01-17 23:26:35 +09:00
Eai
473280d9d2 Linux環境で権限がないためファイルを書き込めない問題を修正 (#49) 2019-01-17 22:12:26 +09:00
Eai
73c697f119 構築手順の最後に.envの読み直しを追記 (#47) 2019-01-17 18:15:37 +09:00
shibafu
3a1dc72cf7 チェックイン修正時のバリデーションエラーで新規作成に飛ばされてしまうバグの修正 (#44)
refs #34
2019-01-17 01:01:42 +09:00
Eai
b04f167709 パブリックタイムラインと検索画面で出るチェックインユーザーの名前をdisplay_nameにする (#39)
現状では設定できないため同じだがこちらのほうが好ましいはず。
2019-01-17 00:12:25 +09:00
shibafu
a3b328b55f メール欄のtypeをemailに変更 (#37)
* ログインフォームのメール欄のtypeをemailに変更

* 新規登録画面のメール欄のtypeをemailに変更

* パスワード再発行画面のメール欄のtypeをemailに変更

* パスワード再発行(実行)画面のメール欄のtypeをemailに変更
2019-01-17 00:00:33 +09:00
shibafu
497c19d06d パスワード再発行(実行)画面のメール欄のtypeをemailに変更 2019-01-16 23:57:30 +09:00
eai04191
ade52f40f4 パスワード再発行画面のメール欄のtypeをemailに変更 2019-01-16 23:20:19 +09:00
eai04191
c38d3fa799 新規登録画面のメール欄のtypeをemailに変更 2019-01-16 23:20:06 +09:00
shibafu
2a91aac569 chmod +x 2019-01-16 22:57:26 +09:00
eai04191
f367ec212f リンクアイコンの親要素をflexboxにしてリンクがアイコンから落ちないようにする 2019-01-16 19:17:30 +09:00
eai04191
735c7c289a Bootstrap v4.1.1 から v4.2.1 に更新 2019-01-16 16:33:00 +09:00
eai04191
cffe539832 アイコンの横に名前が来るように修正 2019-01-16 07:00:12 +09:00
eai04191
2191a96cac Bootstrap v4.1.1 から v4.2.1 に更新 2019-01-16 06:59:10 +09:00
eai04191
64f8b47ae0 ログインフォームのメール欄のtypeをemailに変更 2019-01-16 05:42:46 +09:00
shibafu
2ca6c4c60d Dockerコンテナ内にXdebugを導入 (#33)
* コンテナにxdebugをインストール
* env fileをDockerに読ませるようにした
* 環境変数 APP_DEBUG に応じてXdebugをロードしてApacheを起動するようにした
* シェルスクリプトのWindows対策 (.gitattribute)
2019-01-16 00:42:05 +09:00
shibafu
0d4a61ef15 Merge pull request #32 from eai04191/feature-Metadata-Expires 2019-01-16 00:38:10 +09:00
shibafu
cef23a64cb メタデータの再取得判定をチェックイン時の処理にも実装 2019-01-16 00:31:54 +09:00
shibafu
cd26ef6236 メタデータの更新時に多重登録されないようにした 2019-01-16 00:31:54 +09:00
shibafu
810eea2a59 Metadata.expires_atをCarbon化 2019-01-16 00:31:54 +09:00
shibafu
acb9b5821d expires_atに有効な値が設定されている場合のみ、メタデータの有効期限判定を行う 2019-01-16 00:31:54 +09:00
shibafu
5f01cc3430 メタデータエンティティにexpires_atプロパティを追加 2019-01-16 00:31:54 +09:00
eai04191
938a4d6957 metadataテーブルにexpires_atカラムを追加
メタデータの有効期限が現在より過去の場合、メタデータを再取得する
2019-01-15 16:51:50 +09:00
eai04191
a3813f19cf Merge remote-tracking branch 'upstream/develop' into develop 2019-01-15 00:08:40 +09:00
shibafu
faf0755ebd Reformat 2019-01-15 00:05:01 +09:00
shibafu
a2f797cbbe 独断と偏見のphp-cs-fixerルールを追加 2019-01-15 00:02:44 +09:00
shibafu
8c6cc0692c Merge pull request #29 from eai04191/feature-FantiaResolver
FantiaResolverを追加
2019-01-15 00:01:52 +09:00
eai04191
dcf31865a1 拡張子がpngの場合に対応 2019-01-14 23:50:15 +09:00
eai04191
3dedb57fe4 正規表現を修正 2019-01-14 23:46:39 +09:00
eai04191
f134cbefa8 投稿に画像がない場合エラーが発生するのを修正 2019-01-14 23:08:37 +09:00
eai04191
72ab8bf101 FantiaResolverを追加 2019-01-14 22:55:01 +09:00
eai04191
11836ddd43 Merge remote-tracking branch 'upstream/develop' into develop 2019-01-14 21:38:34 +09:00
shibafu
5c6417cdbe Merge pull request #25 from eai04191/feature-PixivResolver
PixivResolverの追加
2019-01-14 18:49:14 +09:00
mohemohe
0e410ef342 composer installでdistから取れるようにする (#26) 2019-01-14 18:37:42 +09:00
eai04191
d6e981ac39 不要なセミコロンの削除 2019-01-14 18:35:34 +09:00
eai04191
98e933b833 変数名をcamelCase に統一 2019-01-14 18:34:33 +09:00
eai04191
6ff247acd7 proxizeを修正
- ドキュメントコメントを修正
- 引数名をより正確なものに修正
2019-01-14 18:27:21 +09:00
eai04191
2d04ed8dd7 thumbnailToMasterUrlを修正
- ドキュメントコメントを修正
- 変数名をより正確なものに修正
2019-01-14 18:27:21 +09:00
eai04191
d359a41033 メソッド名をcamelCaseに変更 2019-01-14 18:27:04 +09:00
shibafu
2299ac3fe7 開発環境向けの措置として、reCAPTCHAは任意設定とする 2019-01-14 18:03:58 +09:00
shibafu
fcdb9d7aba reCAPTCHAの設置 2019-01-14 18:03:58 +09:00
shibafu
e6abcc4402 依存関係にreCAPTCHA用パッケージを追加 2019-01-14 18:03:58 +09:00
eai04191
b0a7504691 Loggingの削除・メソッドの説明をまともに 2019-01-14 16:11:14 +09:00
eai04191
a645cb497f 謝辞を追加 2019-01-14 16:03:46 +09:00
eai04191
6105f6c860 PixivResolverを追加 2019-01-14 16:01:58 +09:00
Eai
9eb42f1991 不要なインポートの削除 (#23) 2019-01-14 14:19:43 +09:00
eai04191
dfea7f2f24 不要なインポートの削除 2019-01-14 11:29:16 +09:00
shibafu
7441c26694 Merge pull request #22 from shikorism/develop
Release 20190114.0950
2019-01-14 09:51:33 +09:00
shibafu
630be41833 Merge pull request #21 from eai04191/feature-DLsiteResolver
DLsiteResolverを追加
2019-01-14 09:39:58 +09:00
shibafu
c4a69cccbe Merge pull request #20 from eai04191/feature-dropdown-user-link
ドロップダウンにプロフィールへのリンクを追加
2019-01-14 08:35:54 +09:00
eai04191
cb3f060ba6 DLsiteResolverを追加 2019-01-14 04:22:40 +09:00
eai04191
4d7b70f9ad ドロップダウンにプロフィールへのリンクを追加 2019-01-14 02:16:14 +09:00
shibafu
3bb6bf718e Merge pull request #18 from eai04191/issue-5
#17 開発環境の構築に関するメモを追加
2019-01-14 01:36:19 +09:00
eai04191
fc709a6624 READMEに開発環境の構築方法を追加 2019-01-14 01:23:19 +09:00
eai04191
8d5363e978 ComposerのためにDockerfileにgitを追加 2019-01-14 01:23:07 +09:00
eai04191
71137e9ab4 余分なスペースの削除 2019-01-14 01:02:35 +09:00
shibafu
f7a95befbe オカズリンクの上限を2000文字に拡張 (#16)
* マイグレーションのために依存関係を追加
* Ejaculations.linkのデータ型をTEXTに変更
* オカズリンクに2000文字の上限値を設定
2019-01-13 23:58:11 +09:00
shibafu
908790b53d Merge branch 'develop' 2019-01-12 00:51:18 +09:00
shibafu
20799dd757 お惣菜コーナーの表示件数を少し増やす 2019-01-12 00:14:51 +09:00
shibafu
ca02f21812 チェックイン時のメタデータ取得に失敗した際、ログだけ残して終了する 2019-01-11 22:39:54 +09:00
shibafu
168ef1c5f6 チェックインページのタイトル内日付フォーマットをちょっと変更 2019-01-10 01:39:10 +09:00
shibafu
3c2e475e41 Merge branch 'develop' 2019-01-10 01:36:13 +09:00
shibafu
581c1ed952 メロンから取得したサムネイルからcensored flagらしきパラメータを外す 2019-01-10 01:29:26 +09:00
shibafu
ad5fbc7ada ページタイトルを設定 2019-01-08 23:27:48 +09:00
shibafu
cf1757319e Merge branch 'develop' 2019-01-05 01:05:25 +09:00
shibafu
b39b43e705 時間帯・曜日別チェックイン回数グラフ (#14)
* 曜日別回数グラフの追加
* 時間別回数グラフの追加
2019-01-02 14:44:31 +09:00
shibafu
bcc9f3acda Merge branch 'develop' 2018-12-17 23:27:46 +09:00
shibafu
911957b283 nijieのview_popup.php URLからもIDが引けるので正規化して対応 2018-12-17 23:27:19 +09:00
shibafu
b9e29cc283 Merge branch 'develop' 2018-12-14 00:36:58 +09:00
shibafu
316b453cca Laravel 5.5.44にアップデート
ついでにcomposer.jsonをlaravel/laravel:5.5の現状に合わせた
2018-12-14 00:25:19 +09:00
shibafu
e6b28993de parsedown/laravelへの依存を削除し、同等のBladeディレクティブを移植 2018-12-13 23:49:26 +09:00
shibafu
e875d5da02 Merge branch 'develop' 2018-11-20 23:33:07 +09:00
shibafu
233a54eb3e Iwaraのカード表示対応
refs #12
2018-11-20 23:31:57 +09:00
shibafu
515e24c4e4 Merge branch 'develop' 2018-09-11 23:22:29 +09:00
shibafu
af4b60d6e1 LaravelドキュメントのDeploymentsに書かれているコマンドを実行するスクリプト 2018-09-11 23:19:41 +09:00
shibafu
5bb44ab232 検索ボックスにとりあえずrequiredを付けた 2018-09-11 23:10:30 +09:00
shibafu
874e88cc56 プロフィールの「よく使っているタグ」を検索リンク化 2018-09-11 23:08:11 +09:00
shibafu
bdb1640ceb チェックインのタグを検索リンク化 2018-09-11 23:05:01 +09:00
shibafu
c52046d51e オカズリンク入力欄のオートコンプリートを無効化 2018-09-08 00:19:50 +09:00
shibafu
69f212d705 検索ページの初期実装 2018-09-06 23:48:54 +09:00
shibafu
2441fe78b6 Merge branch 'develop' 2018-06-13 21:56:40 +09:00
shibafu
9646f90ce3 通販サイトのメタデータ対応 2018-06-13 01:00:24 +09:00
shibafu
9bd22d9f77 OGPResolverでTwitter Cardsの解析も行えるようにした (#9) 2018-06-13 01:00:11 +09:00
shibafu
a0e4063c47 なんか変わってた 2018-06-13 00:25:34 +09:00
shibafu
0882063c0b ログアウトメニューを右寄せにした 2018-06-12 22:48:43 +09:00
shibafu
b4e40ab748 Bootstrap 4.1にアップデート (#5) 2018-06-12 22:44:07 +09:00
shibafu
6609965360 Fix typo 2018-06-12 21:52:10 +09:00
shibafu
9705c2ce5a よく使っているタグを表示する機能 2018-06-09 00:30:41 +09:00
shibafu
bd93d9ec24 varchar(255)で収まるわけないので作りなおす 2018-06-08 01:44:39 +09:00
shibafu
55bd35ea49 Merge branch 'develop' 2018-06-08 01:00:18 +09:00
shibafu
4dc4efe10d エロ漫画のタイトルくらいは取りたかった 2018-06-08 00:44:42 +09:00
shibafu
dfe149e969 オカズリンクのデータを保存するようにした (#4) 2018-06-07 23:46:40 +09:00
shibafu
f51aaea94c お惣菜コーナーにも「同じオカズでチェックイン」ボタンを表示する 2018-06-07 02:23:09 +09:00
shibafu
1dea0a077c 他人のチェックインにも「同じオカズでチェックイン」ボタンを表示する (#8) 2018-06-07 02:21:40 +09:00
shibafu
d143dc4d84 ドッカーン 2018-06-05 23:45:29 +09:00
shibafu
e033816eab Merge branch 'develop' 2018-06-02 23:33:47 +09:00
shibafu
503e8ba093 「同じオカズでチェックイン」ボタンの追加 2018-06-02 23:33:16 +09:00
shibafu
d224e6bba4 gifとmp4の場合はサムネイルを取得させない 2018-04-15 23:16:19 +09:00
shibafu
3b2e81818b sp.nijie.infoの画像情報を正しく取得できるようにした
fix #7
2018-04-15 21:46:49 +09:00
shibafu
7ca0acacb4 コンテンツ情報取得の実装をapi.phpから剥がした 2018-04-15 03:29:00 +09:00
shibafu
88456bc609 Merge branch 'develop' 2018-04-11 01:18:01 +09:00
shibafu
0f39b502e8 オカズリンクのサムネイル取得にて、特定のJSONのデコード前に改行コードをエスケープするようにした 2018-04-11 01:17:52 +09:00
shibafu
46f049c2b8 Merge branch 'develop' 2018-03-06 23:50:12 +09:00
shibafu
9cdfadf12c オカズリンクのカード幅を調整
fix #3
2018-03-06 23:49:34 +09:00
shibafu
bc57a482be シコ草を日曜始まりに変更 2018-03-06 23:36:45 +09:00
shibafu
ba53156beb 会員登録リンクをヘッダーに追加
fix #2
2018-03-06 23:36:41 +09:00
Shibafu
5b2427a2c9 チェックイン画面でクエリパラメータを受け付ける (#6)
* チェックイン画面でクエリパラメータを受け付けるようにした

* バリデーションエラー吐いたときにクエリパラメータが蘇らないようにした
2018-03-06 22:43:49 +09:00
shibafu
277ee90379 お惣菜コーナーのユーザーURLを正しいものに修正 2018-01-09 21:56:59 +09:00
124 changed files with 8937 additions and 14469 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
.idea
.git
.gitignore
.gitattributes

View File

@@ -5,12 +5,12 @@ APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=tissue
DB_USERNAME=tissue
DB_PASSWORD=tissue
BROADCAST_DRIVER=log
CACHE_DRIVER=file
@@ -35,3 +35,8 @@ SPARKPOST_SECRET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
# (Optional) reCAPTCHA Key
# https://www.google.com/recaptcha
NOCAPTCHA_SECRET=
NOCAPTCHA_SITEKEY=

1
.gitattributes vendored
View File

@@ -3,3 +3,4 @@
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
*.sh text eol=lf

6
.gitignore vendored
View File

@@ -10,4 +10,8 @@ Homestead.yaml
npm-debug.log
yarn-error.log
.env
*.iml
*.iml
.php_cs
.php_cs.cache
.phpstorm.meta.php
_ide_helper*.php

27
.php_cs.dist Normal file
View File

@@ -0,0 +1,27 @@
<?php
return \PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'array_syntax' => [
'syntax' => 'short'
],
'blank_line_before_return' => true,
'function_typehint_space' => true,
'method_separation' => true,
'ordered_imports' => true,
'return_type_declaration' => true,
'new_with_braces' => true,
'no_empty_statement' => true,
'standardize_not_equals' => true,
'single_quote' => true
])
->setFinder(
\PhpCsFixer\Finder::create()
->exclude('bootstrap/cache')
->exclude('resources/views')
->exclude('storage')
->exclude('vendor')
->exclude('node_modules')
->in(__DIR__)
);

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM php:7.1-apache
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
RUN apt-get update \
&& apt-get install -y git libpq-dev unzip \
&& docker-php-ext-install pdo_pgsql \
&& pecl install xdebug \
&& curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer \
&& 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 \
&& a2enmod rewrite
COPY dist/bin /usr/local/bin/
COPY dist/php.d /usr/local/etc/php/php.d/
ENTRYPOINT ["tissue-entrypoint.sh"]
CMD ["apache2-foreground"]
WORKDIR /var/www/html

View File

@@ -1,19 +1,74 @@
Tissue
====
# Tissue
a.k.a. shikorism.net
シコリズムネットにて提供している夜のライフログサービスです。
シコリズムネットにて提供している夜のライフログサービスです。
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
## 構成
* Laravel 5.5
* Bootstrap 4.0
- Laravel 5.5
- Bootstrap 4.2.1
## 実行環境
* PHP 7.1
* PostgreSQL 9.6
- PHP 7.1
- PostgreSQL 9.6
## 開発環境の構築
Docker を用いた開発環境の構築方法です。
1. `.env` ファイルを用意します。`.env.example` をコピーすることで用意ができます。
2. Docker イメージをビルドします
```
docker-compose build
```
3. Docker コンテナを起動します。
```
docker-compose up -d
```
4. Composer を使い必要なライブラリをインストールします。
```
docker-compose exec web composer install
```
5. 暗号化キーの作成と、データベースのマイグレーションを行います。
```
docker-compose exec web php artisan key:generate
docker-compose exec web php artisan migrate
```
6. ファイルに書き込めるように権限を設定します。
```
docker-compose exec web chown -R www-data /var/www/html
```
7. 最後に `.env` を読み込み直すために起動し直します。
```
docker-compose up -d
```
これで準備は完了です。Tissue が動いていれば `http://localhost:4545/` でアクセスができます。
## デバッグ実行
```
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
```
で起動することにより、DB のポート`5432`を開放してホストマシンから接続できるようになります。
## 環境構築上の諸注意
* 初版時点では、DBサーバとしてPostgreSQLを使うよう .env ファイルを設定するくらいです。
当分、PostgreSQLから変える気はないので専用SQL等を平気で使います。
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。
当分、PostgreSQL から変える気はないので専用 SQL 等を平気で使います。

View File

@@ -27,4 +27,11 @@ class Ejaculation extends Model
{
return $this->belongsToMany('App\Tag')->withTimestamps();
}
public function textTags()
{
return implode(' ', $this->tags->map(function ($v) {
return $v->name;
})->all());
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Events;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LinkDiscovered
{
use Dispatchable, SerializesModels;
public $url;
/**
* Create a new event instance.
*
* @param string $url
*/
public function __construct(string $url)
{
$this->url = $url;
}
}

View File

@@ -10,4 +10,4 @@ class Formatter extends Facade
{
return \App\Utilities\Formatter::class;
}
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Http\Controllers\Auth;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
@@ -47,11 +47,20 @@ class RegisterController extends Controller
*/
protected function validator(array $data)
{
return Validator::make($data, [
$rules = [
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
],
'password' => 'required|string|min:6|confirmed'
];
// reCAPTCHAのキーが設定されている場合、判定を有効化
if (!empty(config('captcha.secret'))) {
$rules['g-recaptcha-response'] = 'required|captcha';
}
return Validator::make(
$data,
$rules,
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
['name' => 'ユーザー名']
);

View File

@@ -2,10 +2,10 @@
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{

View File

@@ -2,19 +2,29 @@
namespace App\Http\Controllers;
use App\Ejaculation;
use App\Events\LinkDiscovered;
use App\Tag;
use App\User;
use Carbon\Carbon;
use Validator;
use App\Ejaculation;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Validator;
class EjaculationController extends Controller
{
public function create()
public function create(Request $request)
{
return view('ejaculation.checkin');
$defaults = [
'date' => $request->input('date', date('Y/m/d')),
'time' => $request->input('time', date('H:i')),
'link' => $request->input('link', ''),
'tags' => $request->input('tags', ''),
'note' => $request->input('note', ''),
'is_private' => $request->input('is_private', 0) == 1
];
return view('ejaculation.checkin')->with('defaults', $defaults);
}
public function store(Request $request)
@@ -24,11 +34,11 @@ class EjaculationController extends Controller
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
}
Validator::make($inputs, [
$validator = Validator::make($inputs, [
'date' => 'required|date_format:Y/m/d',
'time' => 'required|date_format:H:i',
'note' => 'nullable|string|max:500',
'link' => 'nullable|url',
'link' => 'nullable|url|max:2000',
'tags' => 'nullable|string',
])->after(function ($validator) use ($request, $inputs) {
// 日時の重複チェック
@@ -38,7 +48,11 @@ class EjaculationController extends Controller
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
}
}
})->validate();
});
if ($validator->fails()) {
return redirect()->route('checkin')->withErrors($validator)->withInput();
}
$ejaculation = Ejaculation::create([
'user_id' => Auth::id(),
@@ -58,6 +72,10 @@ class EjaculationController extends Controller
}
$ejaculation->tags()->sync($tagIds);
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
}
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
}
@@ -86,6 +104,7 @@ class EjaculationController extends Controller
public function edit($id)
{
$ejaculation = Ejaculation::findOrFail($id);
return view('ejaculation.edit')->with(compact('ejaculation'));
}
@@ -98,11 +117,11 @@ class EjaculationController extends Controller
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
}
Validator::make($inputs, [
$validator = Validator::make($inputs, [
'date' => 'required|date_format:Y/m/d',
'time' => 'required|date_format:H:i',
'note' => 'nullable|string|max:500',
'link' => 'nullable|url',
'link' => 'nullable|url|max:2000',
'tags' => 'nullable|string',
])->after(function ($validator) use ($id, $request, $inputs) {
// 日時の重複チェック
@@ -112,7 +131,11 @@ class EjaculationController extends Controller
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
}
}
})->validate();
});
if ($validator->fails()) {
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
}
$ejaculation->fill([
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
@@ -131,6 +154,10 @@ class EjaculationController extends Controller
}
$ejaculation->tags()->sync($tagIds);
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
}
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
}
@@ -140,6 +167,7 @@ class EjaculationController extends Controller
$user = User::findOrFail($ejaculation->user_id);
$ejaculation->tags()->detach();
$ejaculation->delete();
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
}
}
}

View File

@@ -36,6 +36,31 @@ class HomeController extends Controller
$categories = Information::CATEGORIES;
if (Auth::check()) {
// チェックイン動向グラフ用のデータ取得
$groupByDay = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
count(*) AS "count"
SQL
))
->join('users', function ($join) {
$join->on('users.id', '=', 'ejaculations.user_id')
->where('users.accept_analytics', true);
})
->where('ejaculated_date', '>=', now()->subDays(30))
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->get()
->mapWithKeys(function ($item) {
return [$item['date'] => $item['count']];
});
$globalEjaculationCounts = [];
$day = Carbon::now()->subDays(29);
for ($i = 0; $i < 30; $i++) {
$globalEjaculationCounts[$day->format('Y/m/d') . ' の総チェックイン数'] = $groupByDay[$day->format('Y/m/d')] ?? 0;
$day->addDay();
}
// お惣菜コーナー用のデータ取得
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
->where('users.is_protected', false)
@@ -44,10 +69,10 @@ class HomeController extends Controller
->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*')
->with('user', 'tags')
->take(5)
->take(10)
->get();
return view('home')->with(compact('informations', 'categories', 'publicLinkedEjaculations'));
return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
} else {
return view('guest')->with(compact('informations', 'categories'));
}

View File

@@ -14,6 +14,7 @@ class InfoController extends Controller
->orderByDesc('pinned')
->orderByDesc('created_at')
->paginate(20);
return view('info.index')->with([
'informations' => $informations,
'categories' => Information::CATEGORIES
@@ -23,6 +24,7 @@ class InfoController extends Controller
public function show($id)
{
$information = Information::findOrFail($id);
return view('info.show')->with([
'info' => $information,
'category' => Information::CATEGORIES[$information->category]

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Ejaculation;
use App\Tag;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function index(Request $request)
{
$inputs = $request->validate([
'q' => 'required'
]);
$results = Ejaculation::query()
->whereHas('tags', function ($query) use ($inputs) {
$query->where('name', 'like', "%{$inputs['q']}%");
})
->where('is_private', false)
->orderBy('ejaculated_date', 'desc')
->with(['user', 'tags'])
->paginate(20)
->appends($inputs);
return view('search.index')->with(compact('inputs', 'results'));
}
public function relatedTag(Request $request)
{
$inputs = $request->validate([
'q' => 'required'
]);
$results = Tag::query()
->where('name', 'like', "%{$inputs['q']}%")
->paginate(50)
->appends($inputs);
return view('search.relatedTag')->with(compact('inputs', 'results'));
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class SettingController extends Controller
{
public function profile()
{
return view('setting.profile');
}
public function updateProfile(Request $request)
{
$inputs = $request->all();
$validator = Validator::make($inputs, [
'display_name' => 'required|string|max:20',
'bio' => 'nullable|string|max:160',
'url' => 'nullable|url|max:2000'
], [], [
'display_name' => '名前',
'bio' => '自己紹介',
'url' => 'URL'
]);
if ($validator->fails()) {
return redirect()->route('setting')->withErrors($validator)->withInput();
}
$user = Auth::user();
$user->display_name = $inputs['display_name'];
$user->bio = $inputs['bio'] ?? '';
$user->url = $inputs['url'] ?? '';
$user->save();
return redirect()->route('setting')->with('status', 'プロフィールを更新しました。');
}
public function privacy()
{
return view('setting.privacy');
}
public function updatePrivacy(Request $request)
{
$inputs = $request->all(['is_protected', 'accept_analytics']);
$user = Auth::user();
$user->is_protected = $inputs['is_protected'] ?? false;
$user->accept_analytics = $inputs['accept_analytics'] ?? false;
$user->save();
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
}
// ( ◠‿◠ )☛ここに気づいたか・・・消えてもらう ▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああああ
// public function password()
// {
// abort(501);
// }
}

View File

@@ -21,7 +21,8 @@ class UserController extends Controller
}
// チェックインの取得
$query = Ejaculation::select(DB::raw(<<<'SQL'
$query = Ejaculation::select(DB::raw(
<<<'SQL'
id,
ejaculated_date,
note,
@@ -39,7 +40,21 @@ SQL
->with('tags')
->paginate(20);
return view('user.profile')->with(compact('user', 'ejaculations'));
// よく使っているタグ
$tagsQuery = DB::table('ejaculations')
->join('ejaculation_tag', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id')
->join('tags', 'ejaculation_tag.tag_id', '=', 'tags.id')
->selectRaw('tags.name, count(*) as count')
->where('ejaculations.user_id', $user->id);
if (!Auth::check() || $user->id !== Auth::id()) {
$tagsQuery = $tagsQuery->where('ejaculations.is_private', false);
}
$tags = $tagsQuery->groupBy('tags.name')
->orderBy('count', 'desc')
->limit(10)
->get();
return view('user.profile')->with(compact('user', 'ejaculations', 'tags'));
}
public function stats($name)
@@ -49,7 +64,8 @@ SQL
abort(404);
}
$groupByDay = Ejaculation::select(DB::raw(<<<'SQL'
$groupByDay = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
count(*) AS "count"
SQL
@@ -59,9 +75,22 @@ SQL
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->get();
$groupByHour = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'HH24') AS "hour",
count(*) AS "count"
SQL
))
->where('user_id', $user->id)
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
->orderBy(DB::raw('1'))
->get();
$dailySum = [];
$monthlySum = [];
$yearlySum = [];
$dowSum = array_fill(0, 7, 0);
$hourlySum = array_fill(0, 24, 0);
// 年間グラフ用の配列初期化
if ($groupByDay->first() !== null) {
@@ -73,7 +102,7 @@ SQL
}
// 月間グラフ用の配列初期化
$month = Carbon::now()->subMonth(11)->firstOfMonth(); // 直近12ヶ月
$month = Carbon::now()->firstOfMonth()->subMonth(11); // 直近12ヶ月
for ($i = 0; $i < 12; $i++) {
$monthlySum[$month->format('Y/m')] = 0;
$month->addMonth();
@@ -85,12 +114,18 @@ SQL
$dailySum[$date->timestamp] = $data->count;
$yearlySum[$date->year] += $data->count;
$dowSum[$date->dayOfWeek] += $data->count;
if (isset($monthlySum[$yearAndMonth])) {
$monthlySum[$yearAndMonth] += $data->count;
}
}
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum'));
foreach ($groupByHour as $data) {
$hour = (int)$data->hour;
$hourlySum[$hour] += $data->count;
}
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum', 'dowSum', 'hourlySum'));
}
public function okazu($name)
@@ -101,7 +136,8 @@ SQL
}
// チェックインの取得
$query = Ejaculation::select(DB::raw(<<<'SQL'
$query = Ejaculation::select(DB::raw(
<<<'SQL'
id,
ejaculated_date,
note,

View File

@@ -18,7 +18,7 @@ class RedirectIfAuthenticated
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
return redirect()->route('home');
}
return $next($request);

View File

@@ -7,7 +7,7 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class ProfileComposer
class ProfileStatsComposer
{
public function __construct()
{
@@ -58,4 +58,4 @@ SQL
$view->with(compact('latestEjaculation', 'currentSession', 'summary'));
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Listeners;
use App\Events\LinkDiscovered;
use App\Metadata;
use App\MetadataResolver\MetadataResolver;
use App\Utilities\Formatter;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class LinkCollector
{
/** @var Formatter */
private $formatter;
/** @var MetadataResolver */
private $metadataResolver;
/**
* Create the event listener.
*
* @param Formatter $formatter
* @param MetadataResolver $metadataResolver
*/
public function __construct(Formatter $formatter, MetadataResolver $metadataResolver)
{
$this->formatter = $formatter;
$this->metadataResolver = $metadataResolver;
}
/**
* Handle the event.
*
* @param LinkDiscovered $event
* @return void
*/
public function handle(LinkDiscovered $event)
{
// URLの正規化
$url = $this->formatter->normalizeUrl($event->url);
// 無かったら取得
// TODO: ある程度古かったら再取得とかありだと思う
$metadata = Metadata::find($url);
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
try {
$resolved = $this->metadataResolver->resolve($url);
Metadata::updateOrCreate(['url' => $url], [
'title' => $resolved->title,
'description' => $resolved->description,
'image' => $resolved->image,
'expires_at' => $resolved->expires_at
]);
} catch (TransferException $e) {
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
report($e);
}
}
}
}

17
app/Metadata.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Metadata extends Model
{
public $incrementing = false;
protected $primaryKey = 'url';
protected $keyType = 'string';
protected $fillable = ['url', 'title', 'description', 'image', 'expires_at'];
protected $visible = ['url', 'title', 'description', 'image', 'expires_at'];
protected $dates = ['created_at', 'updated_at', 'expires_at'];
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\MetadataResolver;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Support\Facades\Log;
class ActivityPubResolver implements Resolver, Parser
{
/**
* @var \GuzzleHttp\Client
*/
private $activityClient;
public function __construct()
{
$this->activityClient = new \GuzzleHttp\Client([
'headers' => [
'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
]
]);
}
public function resolve(string $url): Metadata
{
$res = $this->activityClient->get($url);
if ($res->getStatusCode() === 200) {
return $this->parse($res->getBody());
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
public function parse(string $json): Metadata
{
$activityOrObject = json_decode($json, true);
$object = $activityOrObject['object'] ?? $activityOrObject;
$metadata = new Metadata();
$metadata->title = isset($object['attributedTo']) ? $this->getTitleFromActor($object['attributedTo']) : '';
$metadata->description .= isset($object['summary']) ? $object['summary'] . " | " : '';
$metadata->description .= isset($object['content']) ? $this->html2text($object['content']) : '';
$metadata->image = $object['attachment'][0]['url'] ?? '';
return $metadata;
}
private function getTitleFromActor(string $url): string
{
try {
$res = $this->activityClient->get($url);
if ($res->getStatusCode() !== 200) {
Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
return '';
}
$actor = json_decode($res->getBody(), true);
$title = $actor['name'] ?? '';
if (isset($actor['preferredUsername'])) {
$title .= ' (@' . $actor['preferredUsername'] . '@' . parse_url($actor['id'], PHP_URL_HOST) . ')';
}
return $title;
} catch (TransferException $e) {
Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
return '';
}
}
private function html2text(string $html): string
{
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
$html = preg_replace('~<br\s*/?\s*>|</p>\s*<p[^>]*>~i', "\n", $html);
$dom = new \DOMDocument();
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
return $dom->textContent;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\MetadataResolver;
use Carbon\Carbon;
use GuzzleHttp\Client;
class CienResolver extends MetadataResolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
// 画像URLから有効期限の起点を拾う
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $params);
if (empty($params['px-time'])) {
throw new \RuntimeException('Parameter "px-time" not found. Image=' . $metadata->image . ' Source=' . $url);
}
$metadata->expires_at = Carbon::createFromTimestamp($params['px-time'])->addHour(1);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class DLsiteResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$metadata->image = str_replace('img_sam.jpg', 'img_main.jpg', $metadata->image);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class DeviantArtResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$node = $xpath->query('//*[@id="pimp-preload"]/following-sibling::div//img')->item(0);
$srcset = $node->getAttribute('srcset');
$srcset_array = explode('w,', $srcset);
$src = end($srcset_array);
$src = preg_replace('~ \d+w$~', '', $src);
if (preg_match('~\.wixmp\.com$~', parse_url($src)['host'])) {
// アスペクト比を保ったまま、縦か横が最大700pxになるように変換する。
// Ref: https://support.wixmp.com/en/article/image-service-3835799
if (strpos($src, '/v1/fill/')) {
$src = preg_replace('~/v1/fill/w_\d+,h_\d+,q_\d+,strp~', '/v1/fit/w_700,h_700,q_70,strp', $src);
} else {
$src = $src . '/v1/fit/w_700,h_700,q_70,strp/image.jpg';
}
}
$metadata->image = $src;
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class FantiaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
preg_match("~\d+~", $url, $match);
$postId = $match[0];
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$node = $xpath->query("//meta[@property='twitter:image']")->item(0);
$ogpUrl = $node->getAttribute('content');
// 投稿に画像がない場合ogp.jpgでない場合のみ大きい画像に変換する
if ($ogpUrl != 'http://fantia.jp/images/ogp.jpg') {
preg_match("~https://fantia\.s3\.amazonaws\.com/uploads/post/file/{$postId}/ogp_(.*?)\.(jpg|png)~", $ogpUrl, $match);
$uuid = $match[1];
$extension = $match[2];
// 大きい画像に変換
$metadata->image = "https://c.fantia.jp/uploads/post/file/{$postId}/main_{$uuid}.{$extension}";
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class FanzaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $metadata->image);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class IwaraResolver implements Resolver
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$metadata = new Metadata();
// find title
foreach ($xpath->query('//title') as $node) {
$content = $node->textContent;
if (!empty($content)) {
$metadata->title = $content;
break;
}
}
// find thumbnail
foreach ($xpath->query('//*[@id="video-player"]') as $node) {
$poster = $node->getAttribute('poster');
if (!empty($poster)) {
if (strpos($poster, '//') === 0) {
$poster = 'https:' . $poster;
}
$metadata->image = $poster;
break;
}
}
if (empty($metadata->image)) {
// YouTube embedded?
foreach ($xpath->query('//div[@class="embedded-video"]//iframe') as $node) {
$src = $node->getAttribute('src');
if (preg_match('~youtube\.com/embed/(\S+)\?~', $src, $matches) !== -1) {
$youtubeId = $matches[1];
$iwaraThumbUrl = 'https://i.iwara.tv/sites/default/files/styles/thumbnail/public/video_embed_field_thumbnails/youtube/' . $youtubeId . '.jpg';
$metadata->image = $iwaraThumbUrl;
break;
}
}
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class KomifloResolver implements Resolver
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
if (preg_match('~komiflo\.com(?:/#!)?/comics/(\\d+)~', $url, $matches) !== 1) {
throw new \RuntimeException("Unmatched URL Pattern: $url");
}
$id = $matches[1];
$res = $this->client->get('https://api.komiflo.com/content/id/' . $id);
if ($res->getStatusCode() === 200) {
$json = json_decode($res->getBody()->getContents(), true);
$metadata = new Metadata();
$metadata->title = $json['content']['data']['title'] ?? '';
$metadata->description = ($json['content']['attributes']['artists']['children'][0]['data']['name'] ?? '?') .
' - ' .
($json['content']['parents'][0]['data']['title'] ?? '?');
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class MelonbooksResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$cookieJar = CookieJar::fromArray(['AUTH_ADULT' => '1'], 'www.melonbooks.co.jp');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
// censoredフラグの除去
if (mb_strpos($metadata->image, '&c=1') !== false) {
$metadata->image = preg_replace('/&c=1/u', '', $metadata->image);
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\MetadataResolver;
use Carbon\Carbon;
class Metadata
{
public $title = '';
public $description = '';
public $image = '';
/** @var Carbon|null */
public $expires_at = null;
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
class MetadataResolver implements Resolver
{
public $rules = [
'~(((sp\.)?seiga\.nicovideo\.jp/seiga(/#!)?|nico\.ms))/im~' => NicoSeigaResolver::class,
'~nijie\.info/view(_popup)?\.php~' => NijieResolver::class,
'~komiflo\.com(/#!)?/comics/(\\d+)~' => KomifloResolver::class,
'~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class,
'~ec\.toranoana\.jp/tora_r/ec/item/.*~' => ToranoanaResolver::class,
'~iwara\.tv/videos/.*~' => IwaraResolver::class,
'~www\.dlsite\.com/.*/work/=/product_id/..\d+\.html~' => DLsiteResolver::class,
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
'~fantia\.jp/posts/\d+~' => FantiaResolver::class,
'~dmm\.co\.jp/~' => FanzaResolver::class,
'~www\.patreon\.com/~' => PatreonResolver::class,
'~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class,
'~\.syosetu\.com/n\d+[a-z]{2,}~' => NarouResolver::class,
'~ci-en\.jp/creator/\d+/article/\d+~' => CienResolver::class,
];
public $mimeTypes = [
'application/activity+json' => ActivityPubResolver::class,
'application/ld+json' => ActivityPubResolver::class,
'text/html' => OGPResolver::class,
'*/*' => OGPResolver::class
];
public $defaultResolver = OGPResolver::class;
public function resolve(string $url): Metadata
{
foreach ($this->rules as $pattern => $class) {
if (preg_match($pattern, $url) === 1) {
/** @var Resolver $resolver */
$resolver = app($class);
return $resolver->resolve($url);
}
}
$result = $this->resolveWithAcceptHeader($url);
if ($result !== null) {
return $result;
}
if (isset($this->defaultResolver)) {
/** @var Resolver $resolver */
$resolver = app($this->defaultResolver);
return $resolver->resolve($url);
}
throw new \UnexpectedValueException('URL not matched.');
}
public function resolveWithAcceptHeader(string $url): ?Metadata
{
try {
// Rails等はAcceptに */* が入っていると、ブラウザの適当なAcceptヘッダだと判断して全部無視してしまう。
// c.f. https://github.com/rails/rails/issues/9940
// そこでここでは */* を「Acceptヘッダを無視してきたレスポンスよくある」のハンドラとして扱い、
// Acceptヘッダには */* を足さないことにする。
$acceptTypes = array_diff(array_keys($this->mimeTypes), ['*/*']);
$client = new \GuzzleHttp\Client();
$res = $client->request('GET', $url, [
'headers' => [
'Accept' => implode(', ', $acceptTypes)
]
]);
if ($res->getStatusCode() === 200) {
preg_match('/^[^;\s]+/', $res->getHeaderLine('Content-Type'), $matches);
$mimeType = $matches[0];
if (isset($this->mimeTypes[$mimeType])) {
$class = $this->mimeTypes[$mimeType];
$parser = new $class();
return $parser->parse($res->getBody());
}
if (isset($this->mimeTypes['*/*'])) {
$class = $this->mimeTypes['*/*'];
$parser = new $class();
return $parser->parse($res->getBody());
}
} else {
// code < 400 && code !== 200 => fallback
}
} catch (ClientException $e) {
// 406 Not Acceptable は多分Acceptが原因なので無視してフォールバック
if ($e->getResponse()->getStatusCode() !== 406) {
throw $e;
}
} catch (ServerException $e) {
// 5xx は変なAcceptが原因かもしれないので無視してフォールバック
}
return null;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class NarouResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$cookieJar = CookieJar::fromArray(['over18' => 'yes'], '.syosetu.com');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$metadata->description = '';
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
$xpath = new \DOMXPath($dom);
$description = [];
// 作者名
$writerNodes = $xpath->query('//*[contains(@class, "novel_writername")]');
if ($writerNodes->length !== 0 && !empty($writerNodes->item(0)->textContent)) {
$description[] = trim($writerNodes->item(0)->textContent);
}
// あらすじ
$exNodes = $xpath->query('//*[@id="novel_ex"]');
if ($exNodes->length !== 0 && !empty($exNodes->item(0)->textContent)) {
$summary = trim($exNodes->item(0)->textContent);
$description[] = mb_strimwidth($summary, 0, 101, '…'); // 100 + '…'(1)
}
$metadata->description = implode(' / ', $description);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class NicoSeigaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
// ページURLからサムネイルURLに変換
preg_match('~http://(?:(?:sp\\.)?seiga\\.nicovideo\\.jp/seiga(?:/#!)?|nico\\.ms)/im(\\d+)~', $url, $matches);
$metadata->image = "http://lohas.nicoseiga.jp/thumb/${matches[1]}l?";
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class NijieResolver implements Resolver
{
/**
* @var Client
*/
protected $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
if (mb_strpos($url, '//sp.nijie.info') !== false) {
$url = preg_replace('~//sp\.nijie\.info~', '//nijie.info', $url);
}
if (mb_strpos($url, 'view_popup.php') !== false) {
$url = preg_replace('~view_popup\.php~', 'view.php', $url);
}
$client = $this->client;
$res = $client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$dataNode = $xpath->query('//script[substring(@type, string-length(@type) - 3, 4) = "json"]');
foreach ($dataNode as $node) {
// 改行がそのまま入っていることがあるのでデコード前にエスケープが必要
$imageData = json_decode(preg_replace('/\r?\n/', '\n', $node->nodeValue), true);
if (isset($imageData['thumbnailUrl']) && !ends_with($imageData['thumbnailUrl'], '.gif') && !ends_with($imageData['thumbnailUrl'], '.mp4')) {
$metadata->image = preg_replace('~nijie\\.info/.*/nijie_picture/~', 'nijie.info/nijie_picture/', $imageData['thumbnailUrl']);
break;
}
}
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class OGPResolver implements Resolver, Parser
{
/**
* @var Client
*/
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
return $this->parse($res->getBody());
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
public function parse(string $html): Metadata
{
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
$xpath = new \DOMXPath($dom);
$metadata = new Metadata();
$metadata->title = $this->findContent($xpath, '//meta[@*="og:title"]', '//meta[@*="twitter:title"]');
if (empty($metadata->title)) {
$nodes = $xpath->query('//title');
if ($nodes->length !== 0) {
$metadata->title = $nodes->item(0)->textContent;
}
}
$metadata->description = $this->findContent($xpath, '//meta[@*="og:description"]', '//meta[@*="twitter:description"]', '//meta[@name="description"]');
$metadata->image = $this->findContent($xpath, '//meta[@*="og:image"]', '//meta[@*="twitter:image"]');
return $metadata;
}
private function findContent(\DOMXPath $xpath, string ...$expressions)
{
foreach ($expressions as $expression) {
$nodes = $xpath->query($expression);
foreach ($nodes as $node) {
$content = $node->getAttribute('content');
if (!empty($content)) {
return $content;
}
}
}
return '';
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\MetadataResolver;
interface Parser
{
public function parse(string $body): Metadata;
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\MetadataResolver;
use Carbon\Carbon;
use GuzzleHttp\Client;
class PatreonResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $temp);
$expires_at_unixtime = $temp['token-time'];
$expires_at = Carbon::createFromTimestamp($expires_at_unixtime);
$metadata->expires_at = $expires_at;
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class PixivResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
/**
* サムネイル画像 URL から最大長辺 1200px の画像 URL に変換する
*
* @param string $thumbnailUrl サムネイル画像 URL
*
* @return string 1200px の画像 URL
*/
public function thumbnailToMasterUrl(string $thumbnailUrl): string
{
$temp = str_replace('/c/128x128', '', $thumbnailUrl);
$largeUrl = str_replace('square1200.jpg', 'master1200.jpg', $temp);
return $largeUrl;
}
/**
* 直リン可能な pixiv.cat のプロキシ URL に変換する
* HUGE THANKS TO PIXIV.CAT!
*
* @param string $pixivUrl i.pximg URL
*
* @return string i.pixiv.cat URL
*/
public function proxize(string $pixivUrl): string
{
return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl);
}
public function resolve(string $url): Metadata
{
parse_str(parse_url($url, PHP_URL_QUERY), $params);
$illustId = $params['illust_id'];
$page = 0;
// 漫画ページページ数はmanga_bigならあるかも
if ($params['mode'] === 'manga_big' || $params['mode'] === 'manga') {
$page = $params['page'] ?? 0;
// 未ログインでは漫画ページを開けないため、URL を作品ページに変換する
$url = preg_replace('~mode=manga(_big)?~', 'mode=medium', $url);
}
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$metadata = $this->ogpResolver->parse($res->getBody());
preg_match("~https://i\.pximg\.net/c/128x128/img-master/img/\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}/{$illustId}(_p0)?_square1200\.jpg~", $res->getBody(), $match);
$illustThumbnailUrl = $match[0];
if ($page != 0) {
$illustThumbnailUrl = str_replace('_p0', '_p'.$page, $illustThumbnailUrl);
}
$illustUrl = $this->thumbnailToMasterUrl($illustThumbnailUrl);
$metadata->image = $this->proxize($illustUrl);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\MetadataResolver;
interface Resolver
{
public function resolve(string $url): Metadata;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class ToranoanaResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$cookieJar = CookieJar::fromArray(['adflg' => '0'], 'ec.toranoana.jp');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
if ($res->getStatusCode() === 200) {
return $this->ogpResolver->parse($res->getBody());
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -2,7 +2,10 @@
namespace App\Providers;
use App\MetadataResolver\MetadataResolver;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Parsedown;
class AppServiceProvider extends ServiceProvider
{
@@ -13,7 +16,9 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
//
Blade::directive('parsedown', function ($expression) {
return "<?php echo app('parsedown')->text($expression); ?>";
});
}
/**
@@ -23,6 +28,11 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->singleton(MetadataResolver::class, function ($app) {
return new MetadataResolver();
});
$this->app->singleton('parsedown', function () {
return Parsedown::instance();
});
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{

View File

@@ -2,8 +2,8 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{

View File

@@ -2,8 +2,8 @@
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
@@ -13,9 +13,9 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'App\Events\Event' => [
'App\Listeners\EventListener',
],
'App\Events\LinkDiscovered' => [
'App\Listeners\LinkCollector'
]
];
/**

View File

@@ -2,8 +2,8 @@
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{

View File

@@ -2,7 +2,7 @@
namespace App\Providers;
use App\Http\ViewComposers\ProfileComposer;
use App\Http\ViewComposers\ProfileStatsComposer;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@@ -15,7 +15,7 @@ class ViewComposerServiceProvider extends ServiceProvider
*/
public function boot()
{
View::composer('components.profile', ProfileComposer::class);
View::composer('components.profile-stats', ProfileStatsComposer::class);
}
/**

View File

@@ -2,8 +2,8 @@
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Auth;
class User extends Authenticatable
@@ -36,9 +36,10 @@ class User extends Authenticatable
* @param int $size 画像サイズ
* @return string Gravatar 画像URL
*/
public function getProfileImageUrl($size = 30) : string
public function getProfileImageUrl($size = 30): string
{
$hash = md5(strtolower(trim($this->email)));
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size;
}

View File

@@ -24,6 +24,7 @@ class Formatter
$days = floor($value / 86400);
$hours = floor($value % 86400 / 3600);
$minutes = floor($value % 3600 / 60);
return "{$days}{$hours}時間 {$minutes}";
}
@@ -36,4 +37,42 @@ class Formatter
{
return $this->linkify->processUrls($text);
}
}
/**
* URLを正規化します。
* @param string $url URL
* @return string 正規化されたURL
*/
public function normalizeUrl($url)
{
// Decode
$url = urldecode($url);
// Remove Hashbang
$url = preg_replace('~/#!/~u', '/', $url);
// Sort query parameters
$parts = parse_url($url);
if (!empty($parts['query'])) {
// Remove query parameters
$url = str_replace_last('?' . $parts['query'], '', $url);
if (!empty($parts['fragment'])) {
// Remove fragment identifier
$url = str_replace_last('#' . $parts['fragment'], '', $url);
} else {
// "http://example.com/?query#" の場合 $parts['fragment'] は unset になるので、個別に判定して除去する必要がある
$url = preg_replace('/#\z/u', '', $url);
}
parse_str($parts['query'], $params);
ksort($params);
$url = $url . '?' . http_build_query($params);
if (!empty($parts['fragment'])) {
$url .= '#' . $parts['fragment'];
}
}
return $url;
}
}

View File

@@ -5,19 +5,24 @@
"license": "MIT",
"type": "project",
"require": {
"php": ">=7.0.0",
"php": ">=7.1.0",
"anhskohbo/no-captcha": "^3.0",
"doctrine/dbal": "^2.9",
"fideloper/proxy": "~3.3",
"guzzlehttp/guzzle": "^6.3",
"laravel/framework": "5.5.*",
"laravel/tinker": "~1.0",
"misd/linkify": "^1.1",
"parsedown/laravel": "~1.0"
"misd/linkify": "^1.1"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.1",
"barryvdh/laravel-ide-helper": "^2.5",
"filp/whoops": "~2.0",
"friendsofphp/php-cs-fixer": "^2.14",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~6.0"
"phpunit/phpunit": "~6.0",
"symfony/thanks": "^1.0"
},
"autoload": {
"classmap": [
@@ -42,6 +47,9 @@
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
],
"fix": [
"php-cs-fixer fix"
]
},
"config": {

2603
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePasswordResetsTable extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateEjaculationsTable extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInformationTable extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLinkToEjaculations extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTagsTable extends Migration
{

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMetadataTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('metadata', function (Blueprint $table) {
$table->string('url');
$table->string('title');
$table->string('description');
$table->string('image');
$table->timestamps();
$table->index('url');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('metadata');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RecreateMetadataTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('metadata');
Schema::create('metadata', function (Blueprint $table) {
$table->text('url');
$table->text('title');
$table->text('description');
$table->text('image');
$table->timestamps();
$table->index('url');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('metadata');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeLinkOnEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->text('link')->default('')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->string('link')->default('')->change();
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddExpiresOnMetadata extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('metadata', function (Blueprint $table) {
$table->timestamp('expires_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('metadata', function (Blueprint $table) {
$table->removeColumn('expires_at');
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBioAndUrlToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('bio', 160)->default('');
$table->text('url')->default('');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('bio');
$table->dropColumn('url');
});
}
}

8
dist/bin/tissue-entrypoint.sh vendored Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
if [[ "$APP_DEBUG" == "true" ]]; then
export PHP_INI_SCAN_DIR=":/usr/local/etc/php/php.d"
fi
exec docker-php-entrypoint "$@"

4
dist/php.d/99-xdebug.ini vendored Normal file
View File

@@ -0,0 +1,4 @@
; Dockerでのデバッグ用設定
zend_extension=xdebug.so
xdebug.remote_enable=true
xdebug.remote_host=host.docker.internal

6
docker-compose.debug.yml Normal file
View File

@@ -0,0 +1,6 @@
version: "3"
services:
db:
ports:
- 5432:5432

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
version: "3"
services:
web:
build: .
env_file:
- .env
volumes:
- .:/var/www/html
networks:
- backend
ports:
- 4545:80
restart: always
depends_on:
- db
db:
image: postgres:10-alpine
environment:
POSTGRES_DB: tissue
POSTGRES_USER: tissue
POSTGRES_PASSWORD: tissue
volumes:
- db:/var/lib/postgresql/data
networks:
- backend
restart: always
networks:
backend:
volumes:
db:

5
prepare.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
# https://laravel.com/docs/5.5/deployment
composer install --optimize-autoloader
php artisan config:cache

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,330 +0,0 @@
html {
box-sizing: border-box;
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1rem;
font-weight: normal;
line-height: 1.5;
color: #212529;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: bold;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
a,
area,
button,
[role="button"],
input,
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #868e96;
text-align: left;
caption-side: bottom;
}
th {
text-align: left;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,8 @@
html{box-sizing:border-box;font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}*,::after,::before{box-sizing:inherit}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}
/*!
* Bootstrap Reboot v4.1.1 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

8185
public/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

39
public/css/tissue.css vendored
View File

@@ -15,6 +15,25 @@
overflow-x: auto;
}
.tis-need-agecheck .container {
filter: blur(45px);
pointer-events: none;
}
.container {
transition: filter .15s liner;
}
.list-group-item.no-side-border {
border-left: none;
border-right: none;
border-radius: 0;
}
.list-group-item.border-bottom-only:first-child {
border-top: none;
}
.list-group-item.border-bottom-only {
border-left: none;
border-right: none;
@@ -27,4 +46,24 @@
.timeline-action-item {
margin-left: 16px;
}
.tis-global-count-graph {
height: 90px;
border-bottom: 1px solid rgba(0, 0, 0, .125);
}
.tis-page-selector {
margin-left: -1px;
width: calc(100% + 2px);
height: 100%;
border: 1px solid #dee2e6;
border-radius: 0;
line-height: 1.25;
}
@media (min-width: 992px) {
.tis-sidebar-info {
font-size: small;
}
}

7336
public/js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

55
public/js/tissue.js vendored Normal file
View File

@@ -0,0 +1,55 @@
// app.jsの名はモジュールバンドラーを投入する日まで予約しておく。CSSも同じ。
(function ($) {
$.fn.linkCard = function (options) {
var settings = $.extend({
endpoint: '/api/checkin/card'
}, options);
return this.each(function () {
var $this = $(this);
$.ajax({
url: settings.endpoint,
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $this.find('img');
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} else {
$desc.text(data.description);
}
if (data.image === '') {
$image.hide();
} else {
$image.attr('src', data.image);
}
if (data.title !== '' || data.description !== '' || data.image !== '') {
$this.removeClass('d-none');
}
});
});
};
$.fn.pageSelector = function () {
return this.on('change', function () {
location.href = $(this).find(':selected').data('href');
});
};
})(jQuery);

View File

@@ -99,6 +99,10 @@ return [
'attribute-name' => [
'rule-name' => 'custom-message',
],
'g-recaptcha-response' => [
'required' => '「私はロボットではありません」にチェックを入れてください。',
'captcha' => 'reCAPTCHAチェックに失敗しました。何度試しても解決しない場合、管理者にお問い合わせください。',
],
],
/*
@@ -117,4 +121,4 @@ return [
'password' => 'パスワード',
],
];
];

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', 'ログイン')
@section('content')
<div class="container">
<h2>ログイン</h2>
@@ -11,7 +13,7 @@
<div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required autofocus>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
@@ -25,12 +27,9 @@
<div class="invalid-feedback">{{ $errors->first('password') }}</div>
@endif
</div>
<div class="form-check">
<label class="custom-control custom-checkbox">
<input id="remember" name="rememver" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
<span class="custom-control-indicator"></span>
<span class="custom-control-description">保存する</span>
</label>
<div class="custom-control custom-checkbox mb-3">
<input id="remember" name="remember" class="custom-control-input" type="checkbox" {{ old('remember') ? 'checked' : '' }}>
<label class="custom-control-label" for="remember">保存する</label>
</div>
<button class="btn btn-primary" type="submit">ログイン</button>

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', 'パスワードの再発行')
@section('content')
<div class="container">
<h2>パスワードの再発行</h2>
@@ -14,7 +16,7 @@
<div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required autofocus>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div>

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', 'パスワードの再発行')
@section('content')
<div class="container">
<h2>パスワードの再発行</h2>
@@ -16,7 +18,7 @@
<div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required>
@if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div>

View File

@@ -1,9 +1,21 @@
@extends('layouts.base')
@section('title', '新規登録')
@push('head')
@if (!empty(config('captcha.secret')))
{!! NoCaptcha::renderJs() !!}
@endif
@endpush
@section('content')
<div class="container">
<h2>新規登録</h2>
<hr>
<div class="alert alert-warning">
<p class="mb-0"><strong>注意!</strong> Tissueでは、登録に使用したメールアドレスの <a href="https://ja.gravatar.com/" rel="noreferrer">Gravatar</a> を使用します。</p>
<p class="mb-0">他の場所での活動と紐付いてほしくない場合、使用予定のメールアドレスにGravatarが設定されていないかを確認することを推奨します。</p>
</div>
<div class="row justify-content-center my-5">
<div class="col-lg-6">
<form method="post" action="{{ route('register') }}">
@@ -19,7 +31,7 @@
</div>
<div class="form-group">
<label for="email"><span class="oi oi-envelope-closed"></span> メールアドレス</label>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="text" value="{{ old('email') }}" required>
<input id="email" name="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" type="email" value="{{ old('email') }}" required>
@if ($errors->has('email'))
<div class="invalid-feedback">{{ $errors->first('email') }}</div>
@@ -41,22 +53,26 @@
<h6 class="mb-3">プライバシーに関するオプション (全て任意です)</h6>
<div class="form-group">
<div class="form-check">
<label class="custom-control custom-checkbox">
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
<span class="custom-control-indicator"></span>
<span class="custom-control-description">全てのチェックイン履歴を非公開にする</span>
</label>
<div class="custom-control custom-checkbox mb-2">
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ old('is_protected') ? 'checked' : '' }}>
<label class="custom-control-label" for="protected">全てのチェックイン履歴を非公開にする</label>
</div>
<div class="form-check">
<label class="custom-control custom-checkbox">
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
<span class="custom-control-indicator"></span>
<span class="custom-control-description">匿名での統計にチェックインデータを利用することに同意します</span>
</label>
<div class="custom-control custom-checkbox">
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ old('accept_analytics') ? 'checked' : '' }}>
<label class="custom-control-label" for="accept-analytics">匿名での統計にチェックインデータを利用することに同意します</label>
</div>
</div>
</div>
@if (!empty(config('captcha.secret')))
<div class="form-row ml-1 mt-2 my-4">
<div class="mx-auto">
{!! NoCaptcha::display() !!}
</div>
@if ($errors->has('g-recaptcha-response'))
<div class="invalid-feedback d-block text-center">{{ $errors->first('g-recaptcha-response') }}</div>
@endif
</div>
@endif
<div class="text-center">
<button class="btn btn-primary btn-lg" type="submit">登録</button>

View File

@@ -0,0 +1,9 @@
<div class="card link-card mb-2 px-0 col-12 col-md-6 d-none" style="font-size: small;">
<a class="text-dark card-link" href="{{ $link }}" target="_blank" rel="noopener">
<img src="" alt="Thumbnail" class="card-img-top bg-secondary">
<div class="card-body">
<h6 class="card-title font-weight-bold">タイトル</h6>
<p class="card-text">コンテンツの説明文</p>
</div>
</a>
</div>

View File

@@ -0,0 +1,15 @@
<h6 class="font-weight-bold"><span class="oi oi-timer"></span> 現在のセッション</h6>
@if (isset($currentSession))
<p class="card-text mb-0">{{ $currentSession }}経過</p>
<p class="card-text">({{ $latestEjaculation->ejaculated_date->format('Y/m/d H:i') }} にリセット)</p>
@else
<p class="card-text mb-0">計測がまだ始まっていません</p>
<p class="card-text">(一度チェックインすると始まります)</p>
@endif
<h6 class="font-weight-bold"><span class="oi oi-graph"></span> 概況</h6>
<p class="card-text mb-0">平均記録: {{ Formatter::formatInterval($summary[0]->average) }}</p>
<p class="card-text mb-0">最長記録: {{ Formatter::formatInterval($summary[0]->longest) }}</p>
<p class="card-text mb-0">最短記録: {{ Formatter::formatInterval($summary[0]->shortest) }}</p>
<p class="card-text mb-0">合計時間: {{ Formatter::formatInterval($summary[0]->total_times) }}</p>
<p class="card-text">通算回数: {{ $summary[0]->total_checkins }}</p>

View File

@@ -1,6 +1,6 @@
<div class="card mb-4">
<div class="card-body">
<img src="{{ $user->getProfileImageUrl(64) }}" class="rounded mb-1">
<img src="{{ $user->getProfileImageUrl(128) }}" class="rounded mb-1">
<h4 class="card-title">
<a class="text-dark" href="{{ route('user.profile', ['name' => $user->name]) }}">{{ $user->display_name }}</a>
</h4>
@@ -11,22 +11,28 @@
@endif
</h6>
@if (!$user->is_protected)
<h6 class="font-weight-bold mt-4"><span class="oi oi-timer"></span> 現在のセッション</h6>
@if (isset($currentSession))
<p class="card-text mb-0">{{ $currentSession }}経過</p>
<p class="card-text">({{ $latestEjaculation->ejaculated_date->format('Y/m/d H:i') }} にリセット)</p>
@else
<p class="card-text mb-0">計測がまだ始まっていません</p>
<p class="card-text">(一度チェックインすると始まります)</p>
@endif
{{-- Bio --}}
@if (!empty($user->bio))
<p class="card-text mt-3 mb-0">
{!! Formatter::linkify(nl2br(e($user->bio))) !!}
</p>
@endif
<h6 class="font-weight-bold"><span class="oi oi-graph"></span> 概況</h6>
<p class="card-text mb-0">平均記録: {{ Formatter::formatInterval($summary[0]->average) }}</p>
<p class="card-text mb-0">最長記録: {{ Formatter::formatInterval($summary[0]->longest) }}</p>
<p class="card-text mb-0">最短記録: {{ Formatter::formatInterval($summary[0]->shortest) }}</p>
<p class="card-text mb-0">合計時間: {{ Formatter::formatInterval($summary[0]->total_times) }}</p>
<p class="card-text">通算回数: {{ $summary[0]->total_checkins }}</p>
{{-- URL --}}
@if (!empty($user->url))
<p class="card-text d-flex mt-3">
<span class="oi oi-link-intact mr-1 mt-1"></span>
<a href="{{ $user->url }}" rel="me nofollow noopener" target="_blank" class="text-truncate">{{ preg_replace('~\Ahttps?://~', '', $user->url) }}</a>
</p>
@endif
</div>
</div>
</div>
@if (!$user->is_protected || $user->isMe())
<div class="card mb-4">
<div class="card-body">
@component('components.profile-stats', ['user' => $user])
@endcomponent
</div>
</div>
@endif

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', 'チェックイン')
@section('content')
<div class="container">
<h2>今致してる?</h2>
@@ -13,7 +15,7 @@
<div class="form-group col-sm-6">
<label for="date"><span class="oi oi-calendar"></span> 日付</label>
<input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}"
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? date('Y/m/d') }}" required>
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? $defaults['date'] }}" required>
@if ($errors->has('date'))
<div class="invalid-feedback">{{ $errors->first('date') }}</div>
@@ -22,7 +24,7 @@
<div class="form-group col-sm-6">
<label for="time"><span class="oi oi-clock"></span> 時刻</label>
<input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}"
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? date('H:i') }}" required>
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? $defaults['time'] }}" required>
@if ($errors->has('time'))
<div class="invalid-feedback">{{ $errors->first('time') }}</div>
@@ -36,9 +38,9 @@
</div>
<div class="form-row">
<div class="form-group col-sm-12">
<input name="tags" type="hidden" value="{{ old('tags') }}">
<input name="tags" type="hidden" value="{{ old('tags') ?? $defaults['tags'] }}">
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
<div class="form-control {{ $errors->has('tags') ? ' is-invalid' : '' }}">
<div class="form-control h-auto {{ $errors->has('tags') ? ' is-invalid' : '' }}">
<ul id="tags" class="list-inline d-inline"></ul>
<input id="tagInput" type="text" style="outline: 0; border: 0;">
</div>
@@ -54,7 +56,7 @@
<div class="form-row">
<div class="form-group col-sm-12">
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
<input id="link" name="link" type="text" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') }}">
<input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $defaults['link'] }}">
<small class="form-text text-muted">
オカズのURLを貼り付けて登録することができます。
</small>
@@ -62,11 +64,13 @@
<div class="invalid-feedback">{{ $errors->first('link') }}</div>
@endif
</div>
@component('components.card', ['link' => null])
@endcomponent
</div>
<div class="form-row">
<div class="form-group col-sm-12">
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') }}</textarea>
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $defaults['note'] }}</textarea>
<small class="form-text text-muted">
最大 500 文字
</small>
@@ -78,13 +82,10 @@
<div class="form-row mt-4">
<p>オプション</p>
<div class="form-group col-sm-12">
<div class="form-check">
<label class="custom-control custom-checkbox">
<input name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') ? 'checked' : '' }}>
<span class="custom-control-indicator"></span>
<span class="custom-control-description">
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
</span>
<div class="custom-control custom-checkbox mb-3">
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') || $defaults['is_private'] ? 'checked' : '' }}>
<label class="custom-control-label" for="isPrivate">
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
</label>
</div>
</div>
@@ -150,4 +151,60 @@
$('#tagInput').focus();
});
</script>
<script>
document.addEventListener(
"DOMContentLoaded",
function() { document.getElementById("link").onchange = linkChanged;},
false
);
function linkChanged(event) {
const card = document.querySelector(".link-card");
const url = event.target.value;
hiddenCard(card);
updateCard(card, url);
return null;
}
async function updateCard(card, url) {
const data = await getCardInfo(url);
card.querySelector(".card-title").innerHTML = data.title;
card.querySelector(".card-text").innerHTML = data.description;
if(data.image){
card.querySelector("img").src = data.image;
}else {
card.querySelector("img").classList.add("d-none");
}
card.classList.remove('col-md-6');
showCard(card);
return null;
}
function getCardInfo(url) {
return $.ajax({
url: '/api/checkin/card',
method: 'get',
type: 'json',
data: {
url: url
}
}).then(function (data) {
console.log(data);
return data;
}).catch(function(e) {
console.log(e); // "oh, no!"
return {
title: "Error",
description: "Error",
image: null
};
});
}
function showCard(card) {
card.classList.remove("d-none");
return null;
}
function hiddenCard(card) {
card.classList.add("d-none");
return null;
}
</script>
@endpush

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', 'チェックインの修正')
@section('content')
<div class="container">
<h2>チェックインの修正</h2>
@@ -37,9 +39,9 @@
</div>
<div class="form-row">
<div class="form-group col-sm-12">
<input name="tags" type="hidden" value="{{ old('tags') ?? implode(' ', $ejaculation->tags->map(function ($v) { return $v->name; })->all()) }}">
<input name="tags" type="hidden" value="{{ old('tags') ?? $ejaculation->textTags() }}">
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
<div class="form-control {{ $errors->has('tags') ? ' is-invalid' : '' }}">
<div class="form-control h-auto {{ $errors->has('tags') ? ' is-invalid' : '' }}">
<ul id="tags" class="list-inline d-inline"></ul>
<input id="tagInput" type="text" style="outline: 0; border: 0;">
</div>
@@ -52,21 +54,10 @@
@endif
</div>
</div>
{{--
<div class="form-row">
<div class="form-group col-sm-12">
<label for="tags"><span class="oi oi-tags"></span> タグ</label>
<input id="tags" type="text" class="form-control" placeholder="未実装です" disabled>
<small class="form-text text-muted">
スペース区切りで複数入力できます。
</small>
</div>
</div>
--}}
<div class="form-row">
<div class="form-group col-sm-12">
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
<input id="link" name="link" type="text" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}">
<input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}">
<small class="form-text text-muted">
オカズのURLを貼り付けて登録することができます。
</small>
@@ -90,13 +81,10 @@
<div class="form-row mt-4">
<p>オプション</p>
<div class="form-group col-sm-12">
<div class="form-check">
<label class="custom-control custom-checkbox">
<input name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
<span class="custom-control-indicator"></span>
<span class="custom-control-description">
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
</span>
<div class="custom-control custom-checkbox mb-3">
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
<label class="custom-control-label" for="isPrivate">
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
</label>
</div>
</div>

View File

@@ -1,5 +1,11 @@
@extends('layouts.base')
@if (!$user->isMe() && ($user->is_protected || $ejaculation->is_private))
@section('title', $user->display_name . ' さんのチェックイン')
@else
@section('title', $user->display_name . ' さんのチェックイン (' . $ejaculation->ejaculated_date->format('n月j日 H:i') . ')')
@endif
@section('content')
<div class="container">
<div class="row">
@@ -26,12 +32,13 @@
<!-- span -->
<div class="d-flex justify-content-between">
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
@if ($user->isMe())
<div>
<div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
@if ($user->isMe())
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
</div>
@endif
@endif
</div>
</div>
<!-- tags -->
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
@@ -40,24 +47,19 @@
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
@endif
@foreach ($ejaculation->tags as $tag)
<span class="badge badge-secondary"><span class="oi oi-tag"></span> {{ $tag->name }}</span>
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;">
<a class="text-dark card-link" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">
<img src="" alt="Thumbnail" class="card-img-top bg-secondary">
<div class="card-body">
<h6 class="card-title font-weight-bold">タイトル</h6>
<p class="card-text">コンテンツの説明文</p>
</div>
</a>
</div>
<p class="mb-2">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
<div class="row mx-0">
@component('components.card', ['link' => $ejaculation->link])
@endcomponent
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
@@ -103,42 +105,8 @@
form.submit();
});
$('.link-card').each(function () {
var $this = $(this);
$.ajax({
url: '{{ url('/api/checkin/card') }}',
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $this.find('img');
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} else {
$desc.text(data.description);
}
if (data.image === '') {
$image.hide();
} else {
$image.attr('src', data.image);
}
if (data.title !== '' || data.description !== '' || data.image !== '') {
$this.removeClass('d-none');
}
});
$('.link-card').linkCard({
endpoint: '{{ url('/api/checkin/card') }}'
});
</script>
@endpush

View File

@@ -31,6 +31,9 @@
<div class="list-group list-group-flush">
@foreach($informations as $info)
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
</a>
@endforeach

View File

@@ -7,114 +7,135 @@
<div class="container">
<div class="row">
<div class="col-lg-4">
@component('components.profile', ['user' => Auth::user()])
@endcomponent
</div>
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex flex-row align-items-end mb-4">
<img src="{{ Auth::user()->getProfileImageUrl(48) }}" class="rounded mr-2">
<div class="d-flex flex-column overflow-hidden">
<h5 class="card-title text-truncate">
<a class="text-dark" href="{{ route('user.profile', ['name' => Auth::user()->name]) }}">{{ Auth::user()->display_name }}</a>
</h5>
<h6 class="card-subtitle">
<a class="text-muted" href="{{ route('user.profile', ['name' => Auth::user()->name]) }}">&commat;{{ Auth::user()->name }}</a>
@if (Auth::user()->is_protected)
<span class="oi oi-lock-locked text-muted"></span>
@endif
</h6>
</div>
</div>
@component('components.profile-stats', ['user' => Auth::user()])
@endcomponent
</div>
</div>
<div class="card mb-4">
<div class="card-header">サイトからのお知らせ</div>
<div class="list-group list-group-flush">
<div class="list-group list-group-flush tis-sidebar-info">
@foreach($informations as $info)
<a class="list-group-item" href="{{ route('info.show', ['id' => $info->id]) }}">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
</a>
@endforeach
<a href="{{ route('info') }}" class="list-group-item text-right">お知らせ一覧 &raquo;</a>
</div>
</div>
@if (!empty($publicLinkedEjaculations))
<div class="card mb-4">
<div class="card-header">お惣菜コーナー</div>
<div class="card-body">
<p class="card-text">最近の公開チェックインから、オカズリンク付きのものを表示しています。</p>
</div>
<ul class="list-group list-group-flush">
@foreach ($publicLinkedEjaculations as $ejaculation)
<li class="list-group-item pt-3 pb-3">
<!-- span -->
<div class="d-flex justify-content-between">
<h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user_id]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> &commat;{{ $ejaculation->user->name }}</a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5>
</div>
<!-- tags -->
@if ($ejaculation->tags->isNotEmpty())
<p class="mb-2">
@foreach ($ejaculation->tags as $tag)
<span class="badge badge-secondary"><span class="oi oi-tag"></span> {{ $tag->name }}</span>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="card link-card mb-2 w-50 d-none" style="font-size: small;">
<a class="text-dark card-link" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">
<img src="" alt="Thumbnail" class="card-img-top bg-secondary">
<div class="card-body">
<h6 class="card-title font-weight-bold">タイトル</h6>
<p class="card-text">コンテンツの説明文</p>
</div>
</a>
</div>
<p class="mb-2">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 tis-word-wrap">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
</li>
@endforeach
</ul>
</div>
<div class="col-lg-8">
@if (!empty($globalEjaculationCounts))
<h5>チェックインの動向</h5>
<div class="w-100 mb-4 position-relative tis-global-count-graph">
<canvas id="global-count-graph"></canvas>
</div>
@endif
@if (!empty($publicLinkedEjaculations))
<h5 class="mb-3">お惣菜コーナー</h5>
<p class="text-secondary">最近の公開チェックインから、オカズリンク付きのものを表示しています。</p>
<ul class="list-group">
@foreach ($publicLinkedEjaculations as $ejaculation)
<li class="list-group-item no-side-border pt-3 pb-3 tis-word-wrap">
<!-- span -->
<div class="d-flex justify-content-between">
<h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> {{ $ejaculation->user->display_name }}</a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5>
<div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
</div>
</div>
<!-- tags -->
@if ($ejaculation->tags->isNotEmpty())
<p class="mb-2">
@foreach ($ejaculation->tags as $tag)
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="row mx-0">
@component('components.card', ['link' => $ejaculation->link])
@endcomponent
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 tis-word-wrap">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
</li>
@endforeach
</ul>
@endif
</div>
</div>
</div>
@endsection
@push('script')
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/chart.js@2.7.1/dist/Chart.min.js"></script>
<script>
$('.link-card').each(function () {
var $this = $(this);
$.ajax({
url: '{{ url('/api/checkin/card') }}',
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $this.find('img');
$('.link-card').linkCard({
endpoint: '{{ url('/api/checkin/card') }}'
});
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
new Chart(document.getElementById('global-count-graph').getContext('2d'), {
type: 'bar',
data: {
labels: @json(array_keys($globalEjaculationCounts)),
datasets: [{
data: @json(array_values($globalEjaculationCounts)),
backgroundColor: 'rgba(0, 0, 0, .1)',
borderColor: 'rgba(0, 0, 0, .25)',
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
legend: {
display: false
},
elements: {
line: {}
},
scales: {
xAxes: [{
display: false
}],
yAxes: [{
display: false,
ticks: {
beginAtZero: true
}
}]
}
if (data.description === '') {
$desc.hide();
} else {
$desc.text(data.description);
}
if (data.image === '') {
$image.hide();
} else {
$image.attr('src', data.image);
}
if (data.title !== '' || data.description !== '' || data.image !== '') {
$this.removeClass('d-none');
}
});
}
});
</script>
@endpush

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', 'お知らせ')
@section('content')
<div class="container">
<h2>サイトからのお知らせ</h2>
@@ -7,26 +9,13 @@
<div class="list-group">
@foreach($informations as $info)
<a class="list-group-item border-bottom-only pt-3 pb-3" href="{{ route('info.show', ['id' => $info->id]) }}">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="badge {{ $categories[$info->category]['class'] }}">{{ $categories[$info->category]['label'] }}</span> {{ $info->title }} <small class="text-secondary">- {{ $info->created_at->format('n月j日') }}</small>
</a>
@endforeach
</div>
<ul class="pagination mt-4 justify-content-center">
<li class="page-item {{ $informations->currentPage() === 1 ? 'disabled' : '' }}">
<a class="page-link" href="{{ $informations->previousPageUrl() }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
@for ($i = 1; $i <= $informations->lastPage(); $i++)
<li class="page-item {{ $i === $informations->currentPage() ? 'active' : '' }}"><a href="{{ $informations->url($i) }}" class="page-link">{{ $i }}</a></li>
@endfor
<li class="page-item {{ $informations->currentPage() === $informations->lastPage() ? 'disabled' : '' }}">
<a class="page-link" href="{{ $informations->nextPageUrl() }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
{{ $informations->links(null, ['className' => 'mt-4 justify-content-center']) }}
</div>
@endsection

View File

@@ -1,5 +1,7 @@
@extends('layouts.base')
@section('title', $category['label'] . ': ' . $info->title)
@section('content')
<div class="container">
<nav aria-label="breadcrumb" role="navigation">
@@ -9,7 +11,12 @@
</ol>
</nav>
<h2><span class="badge {{ $category['class'] }}">{{ $category['label'] }}</span> {{ $info->title }}</h2>
<p class="text-secondary"><span class="oi oi-calendar"></span> {{ $info->created_at->format('Y年n月j日') }}</p>
<p class="text-secondary">
@if ($info->pinned)
<span class="badge badge-secondary"><span class="oi oi-pin"></span>ピン留め</span>
@endif
<span class="oi oi-calendar"></span> {{ $info->created_at->format('Y年n月j日') }}
</p>
@parsedown($info->content)
</div>
@endsection

View File

@@ -6,7 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Tissue') }}</title>
@hasSection('title')
<title>@yield('title') - {{ config('app.name', 'Tissue') }}</title>
@else
<title>{{ config('app.name', 'Tissue') }}</title>
@endif
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ asset('css/open-iconic-bootstrap.min.css') }}" rel="stylesheet">
@@ -14,7 +18,18 @@
@stack('head')
</head>
<body>
<body class="{{Auth::check() ? '' : 'tis-need-agecheck'}}">
<noscript class="navbar navbar-light bg-warning">
<div class="container-fluid">
<div class="d-flex flex-column mx-auto">
<p class="m-0 text-dark">Tissueを利用するには、ブラウザのJavaScriptとCookieを有効にする必要があります。</p>
<p class="m-0 text-info">
<a href="https://www.enable-javascript.com/ja/" target="_blank" rel="nofollow noopener">ブラウザでJavaScriptを有効にする方法</a>
<a href="https://www.whatismybrowser.com/guides/how-to-enable-cookies/auto" target="_blank" rel="nofollow noopener">ブラウザでCookieを有効にする方法</a>
</p>
</div>
</div>
</noscript>
<nav class="navbar navbar-expand-lg navbar-light bg-light {{ !Auth::check() && Route::currentRouteName() === 'home' ? '' : 'mb-4'}}">
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
{{ csrf_field() }}
@@ -44,24 +59,43 @@
<a class="nav-link" href="{{ route('ranking') }}">ランキング</a>
</li>--}}
</ul>
<form action="{{ stripos(Route::currentRouteName(), 'search') === 0 ? route(Route::currentRouteName()) : route('search') }}" class="form-inline mr-2">
<div class="input-group">
<input type="search" name="q" class="form-control" placeholder="検索..." value="{{ stripos(Route::currentRouteName(), 'search') === 0 ? $inputs['q'] : '' }}" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit"><span class="oi oi-magnifying-glass" aria-hidden="true"></span><span class="sr-only">検索</span></button>
</div>
</div>
</form>
<form class="form-inline mr-2">
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
</form>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top mr-2">
{{ Auth::user()->display_name }} さん
<img src="{{ Auth::user()->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-top">
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
{{--<a href="#" class="dropdown-item">設定</a>--}}
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
<a href="{{ route('user.profile', ['name' => Auth::user()->name]) }}" class="dropdown-item">
<strong>{{ Auth::user()->display_name }}</strong>
<p class="mb-0 text-muted">
<span>&commat;{{ Auth::user()->name }}</span>
</p>
</a>
<div class="dropdown-divider"></div>
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
</div>
</li>
</ul>
<form class="form-inline">
<a href="{{ route('checkin') }}" class="btn btn-outline-success">チェックイン</a>
</form>
@endauth
@guest
<form class="form-inline ml-auto">
<ul class="navbar-nav ml-auto mr-2">
<li class="nav-item">
<a href="{{ route('register') }}" class="nav-link">会員登録</a>
</li>
</ul>
<form class="form-inline">
<a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a>
</form>
@endguest
@@ -81,21 +115,52 @@
@yield('content')
<footer class="tis-footer mt-4">
<div class="container p-3 p-md-4">
<p>Copyright (c) 2017 shikorism.net</p>
<p>Copyright (c) 2017-2019 shikorism.net</p>
<ul class="list-inline">
<li class="list-inline-item"><a href="https://github.com/shibafu528" class="text-dark">Admin(@shibafu528)</a></li>
<li class="list-inline-item"><a href="https://github.com/shikorism/tissue" class="text-dark">GitHub</a></li>
</ul>
</div>
</footer>
@guest
<div class="modal fade" id="ageCheckModal" tabindex="-1" role="dialog" aria-labelledby="ageCheckModalTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ageCheckModalTitle">Tissue へようこそ!</h5>
</div>
<div class="modal-body">
この先のコンテンツには暴力表現や性描写など、18歳未満の方が閲覧できないコンテンツが含まれています。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">まかせて</button>
<a href="https://cookpad.com" rel="noreferrer" class="btn btn-secondary">ごめん無理</a>
</div>
</div>
</div>
</div>
@endguest
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.js"></script>
<script type="text/javascript" src="{{ asset('js/bootstrap.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('js/tissue.js') }}"></script>
<script>
$(function(){
@guest
if (Cookies.get('agechecked')) {
$('body').removeClass('tis-need-agecheck');
} else {
$('#ageCheckModal').modal({ backdrop: 'static' })
.on('hide.bs.modal', function() {
$('body').removeClass('tis-need-agecheck');
Cookies.set('agechecked', '1', { expires: 365 });
});
}
@endguest
$('[data-toggle="tooltip"]').tooltip();
$('.alert').alert();
$('.tis-page-selector').pageSelector();
@if (session('status'))
setTimeout(function () {
$('#status').alert('close');

Some files were not shown because too many files have changed in this diff Show More