} else {
$channel->twitch_live = true;
$channel->twitch_category = $data['game_id'];
+ $channel->twitch_category_name = $data['game_name'];
+ $channel->twitch_title = $data['title'];
$channel->twitch_viewers = $data['viewer_count'];
}
$channel->save();
public function search(Request $request) {
$validatedData = $request->validate([
+ 'chatting' => 'boolean|nullable',
'id' => 'array',
'id.*' => 'integer|numeric',
'joinable' => 'boolean|nullable',
if (!empty($validatedData['id'])) {
$channels = $channels->whereIn('id', $validatedData['id']);
}
+ if (isset($validatedData['chatting'])) {
+ $channels = $channels->where('chat', '=', !!$validatedData['chatting']);
+ }
if (isset($validatedData['joinable']) && $validatedData['joinable']) {
$channels = $channels->where('twitch_chat', '!=', '');
}
$url = new SitemapUrl();
$url->path = '/horstielog';
$url->lastmod = ChatBotLog::latest()->first()->created_at;
- $url->changefreq = 'daily';
+ $url->changefreq = 'hourly';
$url->priority = 0.5;
$urls[] = $url;
public function broadcastOn($event) {
$channels = [
+ new PublicChannel('Channel'),
new PrivateChannel('Channel.'.$this->id),
];
if (!empty($this->access_key)) {
'guessing_start' => 'datetime',
'languages' => 'array',
'join' => 'boolean',
+ 'twitch_live' => 'boolean',
];
protected $hidden = [
'access_key',
- 'chat',
'chat_commands',
'chat_settings',
'created_at',
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::table('channels', function (Blueprint $table) {
+ $table->string('twitch_category_name')->default('');
+ $table->string('twitch_title')->default('');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('channels', function (Blueprint $table) {
+ $table->dropColumn('twitch_category_name');
+ $table->dropColumn('twitch_title');
+ });
+ }
+};
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Icon from '../common/Icon';
+
+const Item = ({ channel }) => {
+ const classNames = [
+ 'channel-item',
+ channel.twitch_live ? 'is-live' : 'not-live',
+ ];
+ return <a
+ className={classNames.join(' ')}
+ href={channel.stream_link}
+ rel="noreferrer"
+ target="_blank"
+ >
+ <div className="d-flex justify-content-between">
+ <div className="channel-title fs-5">{channel.title}</div>
+ {channel.twitch_live ?
+ <div className="channel-viewers">
+ <Icon.STREAM />
+ {' '}
+ {channel.twitch_viewers}
+ </div>
+ : null}
+ </div>
+ <div className="channel-stream-title">{channel.twitch_title}</div>
+ <div className="channel-category text-muted">
+ <small>{channel.twitch_category_name}</small>
+ </div>
+ </a>;
+};
+
+Item.propTypes = {
+ channel: PropTypes.shape({
+ stream_link: PropTypes.string,
+ title: PropTypes.string,
+ twitch_category_name: PropTypes.string,
+ twitch_live: PropTypes.bool,
+ twitch_title: PropTypes.string,
+ twitch_viewers: PropTypes.number,
+ }),
+};
+
+export default Item;
const Link = ({ channel }) => {
return <Button
href={channel.stream_link}
- rel="noreferer"
+ rel="noreferrer"
target="_blank"
title={channel.title}
variant="outline-twitch"
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Item from './Item';
+
+const List = ({ channels = [] }) => {
+ return <div className="channel-list">
+ {channels.map(channel =>
+ <Item channel={channel} key={channel.id} />
+ )}
+ </div>;
+};
+
+List.propTypes = {
+ channels: PropTypes.arrayOf(PropTypes.shape({
+ })),
+};
+
+export default List;
export const patchWinner = (winners, winner) =>
[winner, ...(winners || []).filter(w => w.uid !== winner.uid)];
+
+export const compareLive = (a, b) => {
+ const a_live = a && a.twitch_live;
+ const b_live = b && b.twitch_live;
+ if (a_live) {
+ if (b_live) {
+ return 0;
+ }
+ return -1;
+ }
+ if (b_live) {
+ return 1;
+ }
+ return 0;
+};
+
+export const compareName = (a, b) => {
+ const a_name = (a && (a.short_name || a.title)) || '';
+ const b_name = (b && (b.short_name || b.title)) || '';
+ return a_name.localeCompare(b_name);
+};
+
+export const compareTitle = (a, b) => {
+ const a_title = (a && a.title) || '';
+ const b_title = (b && b.title) || '';
+ return a_title.localeCompare(b_title);
+};
+
+export const compareViewers = (a, b) => {
+ const a_viewers = (a && a.twitch_live && a.twitch_viewers) || 0;
+ const b_viewers = (b && b.twitch_live && b.twitch_viewers) || 0;
+ return b_viewers - a_viewers;
+};
+
+export const compareHorstieLog = (a, b) => {
+ const live = compareLive(a, b);
+ if (live) return live;
+ const viewers = compareViewers(a, b);
+ if (viewers) return viewers;
+ return compareName(a, b);
+};
wtf: 'WTF',
yes: 'Ja',
},
+ chatChannels: 'Aktive Channel',
chatError: 'Fehler beim Senden',
chatMinAge: 'Mindestalter (in Tagen)',
chatSettings: 'Chat Bot Einstellungen',
wtf: 'WTF',
yes: 'Yes',
},
+ chatChannels: 'Active Channels',
chatError: 'Error sending message',
chatMinAge: 'Min. age (in days)',
chatSettings: 'Chat Bot Settings',
import axios from 'axios';
import React from 'react';
-import { Container } from 'react-bootstrap';
+import { Col, Container, Row } from 'react-bootstrap';
import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
+import ChannelList from '../components/channel/List';
import List from '../components/chat-bot-logs/List';
import ErrorBoundary from '../components/common/ErrorBoundary';
import ErrorMessage from '../components/common/ErrorMessage';
import Loading from '../components/common/Loading';
+import { compareHorstieLog } from '../helpers/Channel';
export const Component = () => {
+ const [channels, setChannels] = React.useState([]);
const [error, setError] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [log, setLog] = React.useState([]);
+ const { t } = useTranslation();
+
React.useEffect(() => {
const ctrl = new AbortController();
if (!log.length) {
};
}, []);
+ React.useEffect(() => {
+ const ctrl = new AbortController();
+ axios
+ .get(`/api/channels`, {
+ signal: ctrl.signal,
+ params: {
+ chatting: 1,
+ },
+ })
+ .then(response => {
+ setChannels(response.data.sort(compareHorstieLog));
+ })
+ .catch(error => {
+ if (!axios.isCancel(error)) {
+ setChannels([]);
+ }
+ });
+ window.Echo.channel(`Channel`)
+ .listen('.ChannelCreated', (e) => {
+ if (e.model.chat) {
+ setChannels(cs => [e.model, ...cs].sort(compareHorstieLog));
+ }
+ })
+ .listen('.ChannelUpdated', (e) => {
+ if (e.model.chat) {
+ setChannels(cs => {
+ if (cs.find(c => c.id === e.model.id)) {
+ return cs
+ .map(c => c.id === e.model.id ? e.model : c)
+ .sort(compareHorstieLog);
+ } else {
+ return [e.model, ...cs].sort(compareHorstieLog);
+ }
+ });
+ } else {
+ setChannels(cs => cs.filter(c => c.id !== e.model.id));
+ }
+ });
+ return () => {
+ ctrl.abort();
+ };
+ }, []);
+
if (loading) {
return <Loading />;
}
<title>Horstie Log</title>
</Helmet>
<ErrorBoundary>
- <List log={log} />
+ <Row>
+ <Col md={9}>
+ <List log={log} />
+ </Col>
+ <Col className="horstielog-channels">
+ <h2 className="fs-4">{t('twitchBot.chatChannels')}</h2>
+ <ChannelList channels={channels} />
+ </Col>
+ </Row>
</ErrorBoundary>
</Container>;
};
background-color: $danger;
}
}
+
+.horstielog-channels {
+ position: relative;
+
+ .channel-list {
+ position: sticky;
+ top: 0;
+ }
+
+ .channel-item {
+ display: block;
+ margin: 1rem 0;
+ padding: 0.5ex 1ex;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 0.5ex;
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ .channel-title, .channel-viewers {
+ color: $twitch;
+ }
+ }
+
+ &.not-live {
+ background: rgba(0, 0, 0, 0.1);
+ }
+ }
+}