mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Merge branch 'master' into custom-highlight-color-tabs
This commit is contained in:
commit
2d93ceed67
223 changed files with 6441 additions and 2926 deletions
|
@ -1,13 +1,13 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Running MACDEPLOYQT"
|
||||
/usr/local/opt/qt/bin/macdeployqt chatterino.app -dmg
|
||||
echo "Creating APP folder"
|
||||
mkdir app
|
||||
echo "Running hdiutil attach on the built DMG"
|
||||
hdiutil attach chatterino.dmg
|
||||
echo "Copying chatterino.app into the app folder"
|
||||
cp -r /Volumes/chatterino/chatterino.app app/
|
||||
echo "Creating DMG with create-dmg"
|
||||
create-dmg --volname Chatterino2 --volicon ../resources/chatterino.icns --icon-size 50 --app-drop-link 0 0 --format UDBZ chatterino-osx.dmg app/
|
||||
echo "DONE"
|
||||
/usr/local/opt/qt/bin/macdeployqt chatterino.app
|
||||
echo "Creating python3 virtual environment"
|
||||
python3 -m venv venv
|
||||
echo "Entering python3 virtual environment"
|
||||
. venv/bin/activate
|
||||
echo "Installing dmgbuild"
|
||||
python3 -m pip install dmgbuild
|
||||
echo "Running dmgbuild.."
|
||||
dmgbuild --settings ./../.CI/dmg-settings.py -D app=./chatterino.app Chatterino2 chatterino-osx.dmg
|
||||
echo "Done!"
|
||||
|
|
247
.CI/dmg-settings.py
Normal file
247
.CI/dmg-settings.py
Normal file
|
@ -0,0 +1,247 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import biplist
|
||||
import os.path
|
||||
|
||||
#
|
||||
# Example settings file for dmgbuild
|
||||
#
|
||||
|
||||
# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg
|
||||
|
||||
# You can actually use this file for your own application (not just TextEdit)
|
||||
# by doing e.g.
|
||||
#
|
||||
# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg
|
||||
|
||||
# .. Useful stuff ..............................................................
|
||||
|
||||
application = defines.get('app', '/Applications/TextEdit.app')
|
||||
appname = os.path.basename(application)
|
||||
|
||||
def icon_from_app(app_path):
|
||||
plist_path = os.path.join(app_path, 'Contents', 'Info.plist')
|
||||
plist = biplist.readPlist(plist_path)
|
||||
icon_name = plist['CFBundleIconFile']
|
||||
icon_root,icon_ext = os.path.splitext(icon_name)
|
||||
if not icon_ext:
|
||||
icon_ext = '.icns'
|
||||
icon_name = icon_root + icon_ext
|
||||
return os.path.join(app_path, 'Contents', 'Resources', icon_name)
|
||||
|
||||
# .. Basics ....................................................................
|
||||
|
||||
# Uncomment to override the output filename
|
||||
# filename = 'test.dmg'
|
||||
|
||||
# Uncomment to override the output volume name
|
||||
# volume_name = 'Test'
|
||||
|
||||
# Volume format (see hdiutil create -help)
|
||||
format = defines.get('format', 'UDBZ')
|
||||
|
||||
# Volume size
|
||||
size = defines.get('size', None)
|
||||
|
||||
# Files to include
|
||||
files = [ application ]
|
||||
|
||||
# Symlinks to create
|
||||
symlinks = { 'Applications': '/Applications' }
|
||||
|
||||
# Volume icon
|
||||
#
|
||||
# You can either define icon, in which case that icon file will be copied to the
|
||||
# image, *or* you can define badge_icon, in which case the icon file you specify
|
||||
# will be used to badge the system's Removable Disk icon
|
||||
#
|
||||
#icon = '/path/to/icon.icns'
|
||||
badge_icon = icon_from_app(application)
|
||||
|
||||
# Where to put the icons
|
||||
icon_locations = {
|
||||
appname: (140, 120),
|
||||
'Applications': (500, 120)
|
||||
}
|
||||
|
||||
# .. Window configuration ......................................................
|
||||
|
||||
# Background
|
||||
#
|
||||
# This is a STRING containing any of the following:
|
||||
#
|
||||
# #3344ff - web-style RGB color
|
||||
# #34f - web-style RGB color, short form (#34f == #3344ff)
|
||||
# rgb(1,0,0) - RGB color, each value is between 0 and 1
|
||||
# hsl(120,1,.5) - HSL (hue saturation lightness) color
|
||||
# hwb(300,0,0) - HWB (hue whiteness blackness) color
|
||||
# cmyk(0,1,0,0) - CMYK color
|
||||
# goldenrod - X11/SVG named color
|
||||
# builtin-arrow - A simple built-in background with a blue arrow
|
||||
# /foo/bar/baz.png - The path to an image file
|
||||
#
|
||||
# The hue component in hsl() and hwb() may include a unit; it defaults to
|
||||
# degrees ('deg'), but also supports radians ('rad') and gradians ('grad'
|
||||
# or 'gon').
|
||||
#
|
||||
# Other color components may be expressed either in the range 0 to 1, or
|
||||
# as percentages (e.g. 60% is equivalent to 0.6).
|
||||
background = 'builtin-arrow'
|
||||
|
||||
show_status_bar = False
|
||||
show_tab_view = False
|
||||
show_toolbar = False
|
||||
show_pathbar = False
|
||||
show_sidebar = False
|
||||
sidebar_width = 180
|
||||
|
||||
# Window position in ((x, y), (w, h)) format
|
||||
window_rect = ((100, 100), (640, 280))
|
||||
|
||||
# Select the default view; must be one of
|
||||
#
|
||||
# 'icon-view'
|
||||
# 'list-view'
|
||||
# 'column-view'
|
||||
# 'coverflow'
|
||||
#
|
||||
default_view = 'icon-view'
|
||||
|
||||
# General view configuration
|
||||
show_icon_preview = False
|
||||
|
||||
# Set these to True to force inclusion of icon/list view settings (otherwise
|
||||
# we only include settings for the default view)
|
||||
include_icon_view_settings = 'auto'
|
||||
include_list_view_settings = 'auto'
|
||||
|
||||
# .. Icon view configuration ...................................................
|
||||
|
||||
arrange_by = None
|
||||
grid_offset = (0, 0)
|
||||
grid_spacing = 100
|
||||
scroll_position = (0, 0)
|
||||
label_pos = 'bottom' # or 'right'
|
||||
text_size = 16
|
||||
icon_size = 128
|
||||
|
||||
# .. List view configuration ...................................................
|
||||
|
||||
# Column names are as follows:
|
||||
#
|
||||
# name
|
||||
# date-modified
|
||||
# date-created
|
||||
# date-added
|
||||
# date-last-opened
|
||||
# size
|
||||
# kind
|
||||
# label
|
||||
# version
|
||||
# comments
|
||||
#
|
||||
list_icon_size = 16
|
||||
list_text_size = 12
|
||||
list_scroll_position = (0, 0)
|
||||
list_sort_by = 'name'
|
||||
list_use_relative_dates = True
|
||||
list_calculate_all_sizes = False,
|
||||
list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added')
|
||||
list_column_widths = {
|
||||
'name': 300,
|
||||
'date-modified': 181,
|
||||
'date-created': 181,
|
||||
'date-added': 181,
|
||||
'date-last-opened': 181,
|
||||
'size': 97,
|
||||
'kind': 115,
|
||||
'label': 100,
|
||||
'version': 75,
|
||||
'comments': 300,
|
||||
}
|
||||
list_column_sort_directions = {
|
||||
'name': 'ascending',
|
||||
'date-modified': 'descending',
|
||||
'date-created': 'descending',
|
||||
'date-added': 'descending',
|
||||
'date-last-opened': 'descending',
|
||||
'size': 'descending',
|
||||
'kind': 'ascending',
|
||||
'label': 'ascending',
|
||||
'version': 'ascending',
|
||||
'comments': 'ascending',
|
||||
}
|
||||
|
||||
# .. License configuration .....................................................
|
||||
|
||||
# Text in the license configuration is stored in the resources, which means
|
||||
# it gets stored in a legacy Mac encoding according to the language. dmgbuild
|
||||
# will *try* to convert Unicode strings to the appropriate encoding, *but*
|
||||
# you should be aware that Python doesn't support all of the necessary encodings;
|
||||
# in many cases you will need to encode the text yourself and use byte strings
|
||||
# instead here.
|
||||
|
||||
# Recognized language names are:
|
||||
#
|
||||
# af_ZA, ar, be_BY, bg_BG, bn, bo, br, ca_ES, cs_CZ, cy, da_DK, de_AT, de_CH,
|
||||
# de_DE, dz_BT, el_CY, el_GR, en_AU, en_CA, en_GB, en_IE, en_SG, en_US, eo,
|
||||
# es_419, es_ES, et_EE, fa_IR, fi_FI, fo_FO, fr_001, fr_BE, fr_CA, fr_CH,
|
||||
# fr_FR, ga-Latg_IE, ga_IE, gd, grc, gu_IN, gv, he_IL, hi_IN, hr_HR, hu_HU,
|
||||
# hy_AM, is_IS, it_CH, it_IT, iu_CA, ja_JP, ka_GE, kl, ko_KR, lt_LT, lv_LV,
|
||||
# mk_MK, mr_IN, mt_MT, nb_NO, ne_NP, nl_BE, nl_NL, nn_NO, pa, pl_PL, pt_BR,
|
||||
# pt_PT, ro_RO, ru_RU, se, sk_SK, sl_SI, sr_RS, sv_SE, th_TH, to_TO, tr_TR,
|
||||
# uk_UA, ur_IN, ur_PK, uz_UZ, vi_VN, zh_CN, zh_TW
|
||||
|
||||
# license = {
|
||||
# 'default-language': 'en_US',
|
||||
# 'licenses': {
|
||||
# # For each language, the text of the license. This can be plain text,
|
||||
# # RTF (in which case it must start "{\rtf1"), or a path to a file
|
||||
# # containing the license text. If you're using RTF,
|
||||
# # watch out for Python escaping (or read it from a file).
|
||||
# 'English': b'''{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820
|
||||
# {\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;}
|
||||
# {\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;}
|
||||
# {\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;}
|
||||
# \\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133
|
||||
# \\deftab720
|
||||
# \\pard\\pardeftab720\\sa160\\partightenfactor0
|
||||
|
||||
# \\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0
|
||||
# \\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\
|
||||
# \\pard\\pardeftab720\\sa160\\partightenfactor0
|
||||
|
||||
# \\fs36 \\cf2 \\strokec2 What is this?\\
|
||||
# \\pard\\pardeftab720\\sa160\\partightenfactor0
|
||||
|
||||
# \\f1\\b0\\fs22 \\cf2 \\strokec2 This is the English license. It says what you are allowed to do with this software.\\
|
||||
# \\
|
||||
# }''',
|
||||
# },
|
||||
# 'buttons': {
|
||||
# # For each language, text for the buttons on the licensing window.
|
||||
# #
|
||||
# # Default buttons and text are built-in for the following languages:
|
||||
# #
|
||||
# # English (en_US), German (de_DE), Spanish (es_ES), French (fr_FR),
|
||||
# # Italian (it_IT), Japanese (ja_JP), Dutch (nl_NL), Swedish (sv_SE),
|
||||
# # Brazilian Portuguese (pt_BR), Simplified Chinese (zh_CN),
|
||||
# # Traditional Chinese (zh_TW), Danish (da_DK), Finnish (fi_FI),
|
||||
# # Korean (ko_KR), Norwegian (nb_NO)
|
||||
# #
|
||||
# # You don't need to specify them for those languages; if you fail to
|
||||
# # specify them for some other language, English will be used instead.
|
||||
|
||||
# 'en_US': (
|
||||
# b'English',
|
||||
# b'Agree',
|
||||
# b'Disagree',
|
||||
# b'Print',
|
||||
# b'Save',
|
||||
# b'If you agree with the terms of this license, press "Agree" to '
|
||||
# b'install the software. If you do not agree, press "Disagree".'
|
||||
# ),
|
||||
# },
|
||||
# }
|
||||
|
14
.github/ISSUE_TEMPLATE/feature-suggestion.md
vendored
14
.github/ISSUE_TEMPLATE/feature-suggestion.md
vendored
|
@ -7,14 +7,8 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
**What should be added?**
|
||||
<!-- A clear and concise description of the requested feature. -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
**Why should it be added?**
|
||||
<!-- A clear and concise description of what motivates you to request this change. -->
|
||||
|
|
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
|
@ -24,8 +24,18 @@ jobs:
|
|||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ../Qt
|
||||
key: ${{ runner.os }}-QtCache-v2
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v1
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
mirror: 'http://mirrors.ocf.berkeley.edu/qt/'
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
|
||||
# WINDOWS
|
||||
- name: Cache conan
|
||||
|
@ -45,8 +55,7 @@ jobs:
|
|||
- name: Install dependencies (Windows)
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
run: |
|
||||
REM We use this source (temporarily?) until choco has updated their version of conan
|
||||
choco install conan -y -s="https://api.bintray.com/nuget/anotherfoxguy/choco-pkg"
|
||||
choco install conan -y
|
||||
|
||||
refreshenv
|
||||
shell: cmd
|
||||
|
|
10
.github/workflows/check-formatting.yml
vendored
10
.github/workflows/check-formatting.yml
vendored
|
@ -1,6 +1,14 @@
|
|||
name: Check formatting
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
17
BUILDING_ON_FREEBSD.md
Normal file
17
BUILDING_ON_FREEBSD.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# FreeBSD
|
||||
|
||||
Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.10 or newer**.
|
||||
|
||||
## FreeBSD 12.1-RELEASE
|
||||
|
||||
Note: This is known to work on FreeBSD 12.1-RELEASE amd64. Chances are
|
||||
high that this also works on older FreeBSD releases, architectures and
|
||||
FreeBSD 13.0-CURRENT.
|
||||
|
||||
1. Install build dependencies from package sources (or build from the
|
||||
ports tree): `# pkg install qt5-core qt5-multimedia qt5-svg
|
||||
qt5-qmake qt5-buildtools gstreamer-plugins-good boost-libs
|
||||
rapidjson`
|
||||
1. go into project directory
|
||||
1. create build folder `$ mkdir build && cd build`
|
||||
1. `$ qmake .. && make`
|
|
@ -29,18 +29,20 @@ Note: This installation will take about 1.5 GB of disk space.
|
|||
|
||||
## OpenSSL
|
||||
### For our websocket library, we need OpenSSL 1.1
|
||||
1. Download OpenSSL for windows, version `1.1.0j`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_0j.exe)**
|
||||
1. Download OpenSSL for windows, version `1.1.1g`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1g.exe)**
|
||||
2. When prompted, install OpenSSL to `C:\local\openssl`
|
||||
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
||||
|
||||
### For Qt SSL, we need OpenSSL 1.0
|
||||
1. Download OpenSSL for windows, version `1.0.2r`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2r.exe)**
|
||||
1. Download OpenSSL for windows, version `1.0.2u`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)**
|
||||
2. When prompted, install it to any arbitrary empty directory.
|
||||
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
||||
4. Copy the OpenSSL 1.0 files from its `\bin` folder to `C:\local\bin` (You will need to create the folder)
|
||||
5. Then copy the OpenSSL 1.1 files from its `\bin` folder to `C:\local\bin` (Overwrite any duplicate files)
|
||||
6. Add `C:\local\bin` to your path folder ([Follow guide here if you don't know how to do it]( https://www.computerhope.com/issues/ch000549.htm#windows8))
|
||||
|
||||
**If the download links above do not work, try downloading similar 1.1.x & 1.0.x versions [here](https://slproweb.com/products/Win32OpenSSL.html). Note: Don't download the "light" installers, they do not have the required files.**
|
||||
|
||||
Note: This installation will take about 200 MB of disk space.
|
||||
|
||||
## Qt
|
||||
|
|
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
|||
# Changelog
|
||||
|
||||
## Unversioned
|
||||
|
||||
- Major: We now support image thumbnails coming from the link resolver. This feature is off by default and can be enabled in the settings with the "Show link thumbnail" setting. This feature also requires the "Show link info when hovering" setting to be enabled (#1664)
|
||||
- Major: Added image upload functionality to i.nuuls.com with an ability to change upload destination. This works by dragging and dropping an image into a split, or pasting an image into the text edit field. (#1332, #1741)
|
||||
- Minor: Clicking on `Open in browser` in a whisper split will now open your whispers on twitch. (#1828)
|
||||
- Minor: Clicking on @mentions will open the User Popup. (#1674)
|
||||
- Minor: You can now open the Twitch User Card by middle-mouse clicking a username. (#1669)
|
||||
- Minor: User Popup now also includes recent user messages (#1729)
|
||||
- Minor: BetterTTV / FrankerFaceZ emote tooltips now also have emote authors' name (#1721)
|
||||
- Minor: Emotes in the emote popup are now sorted in the same order as the tab completion (#1549)
|
||||
- Minor: Removed "Online Logs" functionality as services are shut down (#1640)
|
||||
- Minor: CTRL+F now selects the Find text input field in the Settings Dialog (#1806 #1811)
|
||||
- Minor: CTRL+F now selects the search text input field in the Search Popup (#1812)
|
||||
- Bugfix: Fix preview on hover not working when Animated emotes options was disabled (#1546)
|
||||
- Bugfix: FFZ custom mod badges no longer scale with the emote scale options (#1602)
|
||||
- Bugfix: MacOS updater looked for non-existing fields, causing it to always fail the update check (#1642)
|
||||
- Bugfix: Fixed message menu crashing if the message you right-clicked goes out of scope before you select an action (#1783) (#1787)
|
||||
- Bugfix: Fixed alternate messages flickering in UserInfoPopup when clicking Refresh if there was an odd number of messages in there (#1789 #1810)
|
||||
- Settings open faster
|
||||
- Dev: Fully remove Twitch Chatroom support
|
||||
- Dev: Handle conversion of historical CLEARCHAT messages to NOTICE messages in Chatterino instead of relying on the Recent Messages API to handle it for us. (#1804)
|
||||
|
|
|
@ -32,6 +32,8 @@ git submodule update --init --recursive
|
|||
|
||||
[Building on Mac](../master/BUILDING_ON_MAC.md)
|
||||
|
||||
[Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
|
||||
|
||||
## Code style
|
||||
The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format.
|
||||
|
||||
|
|
|
@ -131,21 +131,16 @@ SOURCES += \
|
|||
src/controllers/commands/CommandController.cpp \
|
||||
src/controllers/commands/CommandModel.cpp \
|
||||
src/controllers/highlights/HighlightBlacklistModel.cpp \
|
||||
src/controllers/highlights/HighlightController.cpp \
|
||||
src/controllers/highlights/HighlightModel.cpp \
|
||||
src/controllers/highlights/HighlightPhrase.cpp \
|
||||
src/controllers/highlights/UserHighlightModel.cpp \
|
||||
src/controllers/ignores/IgnoreController.cpp \
|
||||
src/controllers/ignores/IgnoreModel.cpp \
|
||||
src/controllers/moderationactions/ModerationAction.cpp \
|
||||
src/controllers/moderationactions/ModerationActionModel.cpp \
|
||||
src/controllers/moderationactions/ModerationActions.cpp \
|
||||
src/controllers/notifications/NotificationController.cpp \
|
||||
src/controllers/notifications/NotificationModel.cpp \
|
||||
src/controllers/pings/PingController.cpp \
|
||||
src/controllers/pings/PingModel.cpp \
|
||||
src/controllers/pings/MutedChannelModel.cpp \
|
||||
src/controllers/taggedusers/TaggedUser.cpp \
|
||||
src/controllers/taggedusers/TaggedUsersController.cpp \
|
||||
src/controllers/taggedusers/TaggedUsersModel.cpp \
|
||||
src/debug/Benchmark.cpp \
|
||||
src/main.cpp \
|
||||
|
@ -161,6 +156,7 @@ SOURCES += \
|
|||
src/messages/MessageColor.cpp \
|
||||
src/messages/MessageContainer.cpp \
|
||||
src/messages/MessageElement.cpp \
|
||||
src/messages/SharedMessageBuilder.cpp \
|
||||
src/messages/search/AuthorPredicate.cpp \
|
||||
src/messages/search/LinkPredicate.cpp \
|
||||
src/messages/search/SubstringPredicate.cpp \
|
||||
|
@ -176,17 +172,18 @@ SOURCES += \
|
|||
src/providers/irc/IrcChannel2.cpp \
|
||||
src/providers/irc/IrcCommands.cpp \
|
||||
src/providers/irc/IrcConnection2.cpp \
|
||||
src/providers/irc/IrcMessageBuilder.cpp \
|
||||
src/providers/irc/IrcServer.cpp \
|
||||
src/providers/LinkResolver.cpp \
|
||||
src/providers/twitch/ChatroomChannel.cpp \
|
||||
src/providers/twitch/ChannelPointReward.cpp \
|
||||
src/providers/twitch/api/Helix.cpp \
|
||||
src/providers/twitch/api/Kraken.cpp \
|
||||
src/providers/twitch/IrcMessageHandler.cpp \
|
||||
src/providers/twitch/PartialTwitchUser.cpp \
|
||||
src/providers/twitch/PubsubActions.cpp \
|
||||
src/providers/twitch/PubsubClient.cpp \
|
||||
src/providers/twitch/PubsubHelpers.cpp \
|
||||
src/providers/twitch/TwitchAccount.cpp \
|
||||
src/providers/twitch/TwitchAccountManager.cpp \
|
||||
src/providers/twitch/TwitchApi.cpp \
|
||||
src/providers/twitch/TwitchBadge.cpp \
|
||||
src/providers/twitch/TwitchBadges.cpp \
|
||||
src/providers/twitch/TwitchChannel.cpp \
|
||||
|
@ -223,6 +220,9 @@ SOURCES += \
|
|||
src/util/JsonQuery.cpp \
|
||||
src/util/RapidjsonHelpers.cpp \
|
||||
src/util/StreamLink.cpp \
|
||||
src/util/StreamerMode.cpp \
|
||||
src/util/Twitch.cpp \
|
||||
src/util/NuulsUploader.cpp \
|
||||
src/util/WindowsHelper.cpp \
|
||||
src/widgets/AccountSwitchPopup.cpp \
|
||||
src/widgets/AccountSwitchWidget.cpp \
|
||||
|
@ -235,7 +235,6 @@ SOURCES += \
|
|||
src/widgets/dialogs/IrcConnectionEditor.cpp \
|
||||
src/widgets/dialogs/LastRunCrashDialog.cpp \
|
||||
src/widgets/dialogs/LoginDialog.cpp \
|
||||
src/widgets/dialogs/LogsPopup.cpp \
|
||||
src/widgets/dialogs/NotificationPopup.cpp \
|
||||
src/widgets/dialogs/QualityPopup.cpp \
|
||||
src/widgets/dialogs/SelectChannelDialog.cpp \
|
||||
|
@ -303,6 +302,7 @@ HEADERS += \
|
|||
src/common/DownloadManager.hpp \
|
||||
src/common/Env.hpp \
|
||||
src/common/FlagsEnum.hpp \
|
||||
src/common/IrcColors.hpp \
|
||||
src/common/LinkParser.hpp \
|
||||
src/common/Modes.hpp \
|
||||
src/common/NetworkCommon.hpp \
|
||||
|
@ -327,7 +327,6 @@ HEADERS += \
|
|||
src/controllers/commands/CommandModel.hpp \
|
||||
src/controllers/highlights/HighlightBlacklistModel.hpp \
|
||||
src/controllers/highlights/HighlightBlacklistUser.hpp \
|
||||
src/controllers/highlights/HighlightController.hpp \
|
||||
src/controllers/highlights/HighlightModel.hpp \
|
||||
src/controllers/highlights/HighlightPhrase.hpp \
|
||||
src/controllers/highlights/UserHighlightModel.hpp \
|
||||
|
@ -336,13 +335,10 @@ HEADERS += \
|
|||
src/controllers/ignores/IgnorePhrase.hpp \
|
||||
src/controllers/moderationactions/ModerationAction.hpp \
|
||||
src/controllers/moderationactions/ModerationActionModel.hpp \
|
||||
src/controllers/moderationactions/ModerationActions.hpp \
|
||||
src/controllers/notifications/NotificationController.hpp \
|
||||
src/controllers/notifications/NotificationModel.hpp \
|
||||
src/controllers/pings/PingController.hpp \
|
||||
src/controllers/pings/PingModel.hpp \
|
||||
src/controllers/pings/MutedChannelModel.hpp \
|
||||
src/controllers/taggedusers/TaggedUser.hpp \
|
||||
src/controllers/taggedusers/TaggedUsersController.hpp \
|
||||
src/controllers/taggedusers/TaggedUsersModel.hpp \
|
||||
src/debug/AssertInGuiThread.hpp \
|
||||
src/debug/Benchmark.hpp \
|
||||
|
@ -362,6 +358,7 @@ HEADERS += \
|
|||
src/messages/MessageContainer.hpp \
|
||||
src/messages/MessageElement.hpp \
|
||||
src/messages/MessageParseArgs.hpp \
|
||||
src/messages/SharedMessageBuilder.hpp \
|
||||
src/messages/search/AuthorPredicate.hpp \
|
||||
src/messages/search/LinkPredicate.hpp \
|
||||
src/messages/search/MessagePredicate.hpp \
|
||||
|
@ -380,18 +377,19 @@ HEADERS += \
|
|||
src/providers/irc/IrcChannel2.hpp \
|
||||
src/providers/irc/IrcCommands.hpp \
|
||||
src/providers/irc/IrcConnection2.hpp \
|
||||
src/providers/irc/IrcMessageBuilder.hpp \
|
||||
src/providers/irc/IrcServer.hpp \
|
||||
src/providers/LinkResolver.hpp \
|
||||
src/providers/twitch/ChatroomChannel.hpp \
|
||||
src/providers/twitch/ChannelPointReward.hpp \
|
||||
src/providers/twitch/api/Helix.hpp \
|
||||
src/providers/twitch/api/Kraken.hpp \
|
||||
src/providers/twitch/EmoteValue.hpp \
|
||||
src/providers/twitch/IrcMessageHandler.hpp \
|
||||
src/providers/twitch/PartialTwitchUser.hpp \
|
||||
src/providers/twitch/PubsubActions.hpp \
|
||||
src/providers/twitch/PubsubClient.hpp \
|
||||
src/providers/twitch/PubsubHelpers.hpp \
|
||||
src/providers/twitch/TwitchAccount.hpp \
|
||||
src/providers/twitch/TwitchAccountManager.hpp \
|
||||
src/providers/twitch/TwitchApi.hpp \
|
||||
src/providers/twitch/TwitchBadge.hpp \
|
||||
src/providers/twitch/TwitchBadges.hpp \
|
||||
src/providers/twitch/TwitchChannel.hpp \
|
||||
|
@ -436,9 +434,12 @@ HEADERS += \
|
|||
src/util/LayoutCreator.hpp \
|
||||
src/util/LayoutHelper.hpp \
|
||||
src/util/Overloaded.hpp \
|
||||
src/util/PersistSignalVector.hpp \
|
||||
src/util/PostToThread.hpp \
|
||||
src/util/QObjectRef.hpp \
|
||||
src/util/QStringHash.hpp \
|
||||
src/util/StreamerMode.hpp \
|
||||
src/util/Twitch.hpp \
|
||||
src/util/rangealgorithm.hpp \
|
||||
src/util/RapidjsonHelpers.hpp \
|
||||
src/util/RapidJsonSerializeQString.hpp \
|
||||
|
@ -449,6 +450,7 @@ HEADERS += \
|
|||
src/util/Shortcut.hpp \
|
||||
src/util/StandardItemHelper.hpp \
|
||||
src/util/StreamLink.hpp \
|
||||
src/util/NuulsUploader.hpp \
|
||||
src/util/WindowsHelper.hpp \
|
||||
src/widgets/AccountSwitchPopup.hpp \
|
||||
src/widgets/AccountSwitchWidget.hpp \
|
||||
|
@ -461,7 +463,6 @@ HEADERS += \
|
|||
src/widgets/dialogs/IrcConnectionEditor.hpp \
|
||||
src/widgets/dialogs/LastRunCrashDialog.hpp \
|
||||
src/widgets/dialogs/LoginDialog.hpp \
|
||||
src/widgets/dialogs/LogsPopup.hpp \
|
||||
src/widgets/dialogs/NotificationPopup.hpp \
|
||||
src/widgets/dialogs/QualityPopup.hpp \
|
||||
src/widgets/dialogs/SelectChannelDialog.hpp \
|
||||
|
|
20
docs/Commands.md
Normal file
20
docs/Commands.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
Commands are used as shortcuts for long messages. If a message starts with the "trigger" then the message will be replaced with the Command.
|
||||
|
||||
#### Example
|
||||
Add Command `Hello chat :)` with the trigger `/hello`. Now typing `/hello` in chat will send `Hello chat :)` instead of `/hello`.
|
||||
|
||||
## Advanced
|
||||
|
||||
- The trigger has to be matched at the **start** of the message but there is a setting to also match them at the **end**.
|
||||
- Triggers don't need to start with `/`
|
||||
|
||||
#### Using placeholders
|
||||
- `{1}`, `{2}`, `{3}` and so on can be used to insert the 1st, 2nd, 3rd, ... word after the trigger.
|
||||
|
||||
Example: Add Command `/timeout {1} 1` with trigger `/warn`. Now typing `/warn user123` will send `/timeout user123 1`
|
||||
|
||||
- Similarly `{1+}` and so on can be used to insert all words starting with the 1st, ... word.
|
||||
|
||||
Example: Add Command `Have a {1+} day!` with trigger `/day`. Now typing `/day very super nice` will send `Have a very super nice day!`
|
||||
|
||||
- You can use `{{1}` if you want to send `{1}` literally.
|
|
@ -3,7 +3,7 @@ Below I have tried to list all environment variables that can be used to modify
|
|||
|
||||
### CHATTERINO2_RECENT_MESSAGES_URL
|
||||
Used to change the URL that Chatterino2 uses when trying to load historic Twitch chat messages (if the setting is enabled).
|
||||
Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1?clearchatToNotice=true` (an [open-source service](https://github.com/robotty/recent-messages) written and currently run by [@RAnders00](https://github.com/RAnders00))
|
||||
Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1` (an [open-source service](https://github.com/robotty/recent-messages2) written and currently run by [@RAnders00](https://github.com/RAnders00) - [visit the homepage for more details about the service](https://recent-messages.robotty.de/))
|
||||
Arguments:
|
||||
- `%1` = Name of the Twitch channel
|
||||
|
||||
|
|
47
docs/IMAGEUPLOADER.md
Normal file
47
docs/IMAGEUPLOADER.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
## Image Uploader
|
||||
You can drag and drop images to Chatterino or paste them from clipboard to upload them to an external service.
|
||||
|
||||
By default, images are uploaded to [i.nuuls.com](https://i.nuuls.com).
|
||||
You can change that in `Chatterino Settings -> External Tools -> Image Uploader`.
|
||||
|
||||
Note to advanced users: This module sends multipart-form requests via POST method, so uploading via SFTP/FTP won't work.
|
||||
However, popular hosts like [imgur.com](https://imgur.com) are [s-ul.eu](https://s-ul.eu) supported. Scroll down to see example cofiguration.
|
||||
|
||||
### General settings explanation:
|
||||
|
||||
|Row|Description|
|
||||
|-|-|
|
||||
|Request URL|Link to an API endpoint, which is requested by chatterino. Any needed URL parameters should be included here.|
|
||||
|Form field|Name of a field, which contains image data.|
|
||||
|Extra headers|Extra headers, that will be included in the request. Header name and value must be separated by colon (`:`). Multiple headers need to be separated with semicolons (`;`).<br>Example: `Authorization: supaKey ; NextHeader: value` .|
|
||||
|Image link|Schema that tells where is the link in service's response. Leave empty if server's response is just the link itself. Refer to json properties by `{property}`. Supports dot-notation, example: `{property.anotherProperty}` .|
|
||||
|Deletion link|Same as above.|
|
||||
|
||||
<br>
|
||||
|
||||
## Examples
|
||||
|
||||
### i.nuuls.com
|
||||
|
||||
Simply clear all the fields.
|
||||
|
||||
### imgur.com
|
||||
|
||||
|Row|Description|
|
||||
|-|-|
|
||||
|Request URL|`https://api.imgur.com/3/image`|
|
||||
|Form field|`image`|
|
||||
|Extra headers|`Authorization: Client-ID c898c0bb848ca39`|
|
||||
|Image link|`{data.link}`|
|
||||
|Deletion link|`https://imgur.com/delete/{data.deletehash}`|
|
||||
|
||||
### s-ul.eu
|
||||
|
||||
Replace `XXXXXXXXXXXXXXX` with your API key from s-ul.eu. It can be found on [your account's configuration page](https://s-ul.eu/account/configurations).
|
||||
|Row|Description|
|
||||
|-|-|
|
||||
|Request URL|`https://s-ul.eu/api/v1/upload?wizard=true&key=XXXXXXXXXXXXXXX`|
|
||||
|Form field|`file`|
|
||||
|Extra headers||
|
||||
|Image link|`{url}`|
|
||||
|Deletion link|`https://s-ul.eu/delete.php?file={filename}&key=XXXXXXXXXXXXXXX`|
|
|
@ -1 +1,21 @@
|
|||
# Documents
|
||||
|
||||
Welcome to the Chatterino2 documentation!
|
||||
|
||||
Table of contents:
|
||||
- [Environment variables](ENV.md)
|
||||
- [Commands](Commands.md)
|
||||
- [Regex](Regex.md)
|
||||
- [Notes](notes/README.md)
|
||||
- [TCaccountmanager](notes/TCaccountmanager.md)
|
||||
- [TCappearanceSettings.md](notes/TCappearanceSettings.md)
|
||||
- [TCchannelNavigation.md](notes/TCchannelNavigation.md)
|
||||
- [TCchatterinoFeatures.md](notes/TCchatterinoFeatures.md)
|
||||
- [TCchatting.md](notes/TCchatting.md)
|
||||
- [TCchatWindowNavigation.md](notes/TCchatWindowNavigation.md)
|
||||
- [TCemotes.md](notes/TCemotes.md)
|
||||
- [TCgeneral.md](notes/TCgeneral.md)
|
||||
- [TChighlighting.md](notes/TChighlighting.md)
|
||||
- [TCtabsAndSplits.md](notes/TCtabsAndSplits.md)
|
||||
- [TCusernameTabbing.md](notes/TCusernameTabbing.md)
|
||||
|
||||
|
|
45
docs/Regex.md
Normal file
45
docs/Regex.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
_Regular expressions_ (or short _regexes_) are often used to check if a text matches a certain pattern. For example the regex `ab?c` would match `abc` or `ac`, but not `abbc` or `123`. In Chatterino, you can use them to highlight messages (and more) based on complex conditions.
|
||||
|
||||
Basic patterns:
|
||||
|
||||
|Pattern |Matches|
|
||||
|-|-|
|
||||
|`x?` |nothing or `x`|
|
||||
|`x*` |`x`, repeated any number of times|
|
||||
|`x+` |`x`, repeated any number of times but at least 1|
|
||||
|`^` |The start of the text|
|
||||
|`$` |The end of the text|
|
||||
|`x\|y` |`x` or `y`|
|
||||
|
||||
You can group multiple statements with `()`:
|
||||
|
||||
|Pattern |Matches|
|
||||
|-|-|
|
||||
|`asd?` |`asd` or `as`|
|
||||
|`(asd)?` |`asd` or nothing|
|
||||
|`\(asd\)` |`(asd)`, literally|
|
||||
|
||||
You can also group multiple characters with `[]`:
|
||||
|
||||
|Pattern |Matches|
|
||||
|-|-|
|
||||
|`[xyz]` |`x`, `y` or `z`|
|
||||
|`[1-5a-f]` |`1`,`2`,`3`,`4`,`5`,`a`,`b`,`c`,`d`,`e`,`f`|
|
||||
|`[^abc]` |Anything, **except** `a`, `b` and `c`|
|
||||
|`[\-]` |`-`, literally (escaped with `\`)|
|
||||
|`\[xyz\]` |`[xyz]`, literally|
|
||||
|
||||
Special patterns:
|
||||
|
||||
|Pattern |Matches|
|
||||
|-|-|
|
||||
|`\d` |Digit characters (0-9)|
|
||||
|`\D` |Non-digit characters|
|
||||
|`\w` |Word characters (a-zA-Z0-9_)|
|
||||
|`\W` |Non-word characters|
|
||||
|`\s` |Spaces, tabs, etc.|
|
||||
|`\S` |Not spaces, tabs, etc.|
|
||||
|`\b` |Word boundaries (between \w and \W)|
|
||||
|`\B` |Non-word boundaries|
|
||||
|
||||
You can try out your regex pattern on websites like [https://regex101.com/](regex101.com).
|
|
@ -1 +1 @@
|
|||
Subproject commit 1c38746b05d9311e73c8c8acdfdc4d36c9c551be
|
||||
Subproject commit 6665ccad90461c01b7fe704a98a835953d644156
|
|
@ -29,6 +29,8 @@ TranRed | https://github.com/TranRed | | Contributor
|
|||
RAnders00 | https://github.com/RAnders00 | | Contributor
|
||||
YungLPR | https://github.com/leon-richardt | | Contributor
|
||||
Mm2PL | https://github.com/mm2pl | | Contributor
|
||||
gempir | https://github.com/gempir | | Contributor
|
||||
mfmarlow | https://github.com/mfmarlow | | Contributor
|
||||
# If you are a contributor add yourself above this line
|
||||
|
||||
Defman21 | https://github.com/Defman21 | | Documentation
|
||||
|
|
|
@ -39,7 +39,6 @@ chatterino--TitleLabel {
|
|||
font-family: "Segoe UI light";
|
||||
font-size: 24px;
|
||||
color: #4FC3F7;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
chatterino--DescriptionLabel {
|
||||
|
|
|
@ -28,8 +28,9 @@
|
|||
<file>buttons/unmod.png</file>
|
||||
<file>buttons/update.png</file>
|
||||
<file>buttons/updateError.png</file>
|
||||
<file>com.chatterino.chatterino.desktop</file>
|
||||
<file>chatterino.icns</file>
|
||||
<file>com.chatterino.chatterino.appdata.xml</file>
|
||||
<file>com.chatterino.chatterino.desktop</file>
|
||||
<file>contributors.txt</file>
|
||||
<file>emoji.json</file>
|
||||
<file>emojidata.txt</file>
|
||||
|
@ -50,6 +51,12 @@
|
|||
<file>licenses/websocketpp.txt</file>
|
||||
<file>pajaDank.png</file>
|
||||
<file>qss/settings.qss</file>
|
||||
<file>scrolling/downScroll.png</file>
|
||||
<file>scrolling/downScroll.svg</file>
|
||||
<file>scrolling/neutralScroll.png</file>
|
||||
<file>scrolling/neutralScroll.svg</file>
|
||||
<file>scrolling/upScroll.png</file>
|
||||
<file>scrolling/upScroll.svg</file>
|
||||
<file>settings/about.svg</file>
|
||||
<file>settings/aboutlogo.png</file>
|
||||
<file>settings/accounts.svg</file>
|
||||
|
|
BIN
resources/scrolling/downScroll.png
Normal file
BIN
resources/scrolling/downScroll.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
96
resources/scrolling/downScroll.svg
Normal file
96
resources/scrolling/downScroll.svg
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 8.4666665 8.4666669"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="downScroll.svg"
|
||||
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
|
||||
inkscape:export-xdpi="7.6051788"
|
||||
inkscape:export-ydpi="7.6051788">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="15.839192"
|
||||
inkscape:cx="14.417768"
|
||||
inkscape:cy="15.865661"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1053"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4532" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-288.53332)">
|
||||
<circle
|
||||
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.03761128;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path3719"
|
||||
cx="4.2333331"
|
||||
cy="292.76666"
|
||||
r="4.2145276" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.05049507;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4526"
|
||||
cx="4.2333317"
|
||||
cy="292.76425"
|
||||
r="0.72627902" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4538"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="99.21875"
|
||||
sodipodi:cy="155.44792"
|
||||
sodipodi:r1="13.229166"
|
||||
sodipodi:r2="6.614583"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
|
||||
inkscape:transform-center-x="5.0649804e-06"
|
||||
inkscape:transform-center-y="2.2264812"
|
||||
transform="matrix(0,0.09239709,-0.09239709,0,18.596269,286.19867)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
BIN
resources/scrolling/neutralScroll.png
Normal file
BIN
resources/scrolling/neutralScroll.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
122
resources/scrolling/neutralScroll.svg
Normal file
122
resources/scrolling/neutralScroll.svg
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 8.4666665 8.4666669"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="neutralScroll.svg"
|
||||
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
|
||||
inkscape:export-xdpi="7.6051788"
|
||||
inkscape:export-ydpi="7.6051788">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="15.839192"
|
||||
inkscape:cx="25.340042"
|
||||
inkscape:cy="15.865661"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1053"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4532" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-288.53332)">
|
||||
<g
|
||||
id="g5271"
|
||||
transform="matrix(0.07922061,0,0,0.07922061,-4.1782222,281.17363)">
|
||||
<circle
|
||||
r="53.199886"
|
||||
cy="146.33852"
|
||||
cx="106.17888"
|
||||
id="path3719"
|
||||
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.47476631;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<g
|
||||
transform="matrix(1.7324984,0,0,1.7324984,-77.775866,-107.19273)"
|
||||
id="g4665">
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.36790693;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4526"
|
||||
cx="106.17887"
|
||||
cy="146.32092"
|
||||
r="5.2916665" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4538"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="99.21875"
|
||||
sodipodi:cy="155.44792"
|
||||
sodipodi:r1="13.229166"
|
||||
sodipodi:r2="6.614583"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
|
||||
inkscape:transform-center-x="5.0649804e-06"
|
||||
inkscape:transform-center-y="2.2264812"
|
||||
transform="matrix(0,0.67320493,-0.67320493,0,210.82719,98.484167)" />
|
||||
<path
|
||||
transform="matrix(0,-0.67320493,-0.67320493,0,210.82719,194.19287)"
|
||||
inkscape:transform-center-y="-2.2264854"
|
||||
inkscape:transform-center-x="5.0649804e-06"
|
||||
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
|
||||
inkscape:randomized="0"
|
||||
inkscape:rounded="0"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:arg2="1.0471976"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:r2="6.614583"
|
||||
sodipodi:r1="13.229166"
|
||||
sodipodi:cy="155.44792"
|
||||
sodipodi:cx="99.21875"
|
||||
sodipodi:sides="3"
|
||||
id="path4540"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:type="star" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
BIN
resources/scrolling/upScroll.png
Normal file
BIN
resources/scrolling/upScroll.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
96
resources/scrolling/upScroll.svg
Normal file
96
resources/scrolling/upScroll.svg
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 8.4666665 8.4666669"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="upScroll.svg"
|
||||
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
|
||||
inkscape:export-xdpi="7.6051788"
|
||||
inkscape:export-ydpi="7.6051788">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="15.839192"
|
||||
inkscape:cx="14.417768"
|
||||
inkscape:cy="15.865661"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1053"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4532" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-288.53332)">
|
||||
<circle
|
||||
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.03761128;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path3719"
|
||||
cx="4.2333331"
|
||||
cy="292.76666"
|
||||
r="4.2145276" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.05049507;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4526"
|
||||
cx="4.2333317"
|
||||
cy="292.76425"
|
||||
r="0.72627902" />
|
||||
<path
|
||||
transform="matrix(0,-0.09239709,-0.09239709,0,18.596269,299.33465)"
|
||||
inkscape:transform-center-y="-2.2264854"
|
||||
inkscape:transform-center-x="5.0649804e-06"
|
||||
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
|
||||
inkscape:randomized="0"
|
||||
inkscape:rounded="0"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:arg2="1.0471976"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:r2="6.614583"
|
||||
sodipodi:r1="13.229166"
|
||||
sodipodi:cy="155.44792"
|
||||
sodipodi:cx="99.21875"
|
||||
sodipodi:sides="3"
|
||||
id="path4540"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:type="star" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -5,12 +5,8 @@
|
|||
#include "common/Args.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/CommandController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
#include "controllers/moderationactions/ModerationActions.hpp"
|
||||
#include "controllers/notifications/NotificationController.hpp"
|
||||
#include "controllers/pings/PingController.hpp"
|
||||
#include "controllers/taggedusers/TaggedUsersController.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||
|
@ -31,6 +27,7 @@
|
|||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/IsBigEndian.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
#include "widgets/Notebook.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
#include "widgets/splits/Split.hpp"
|
||||
|
@ -54,16 +51,10 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
|
||||
, accounts(&this->emplace<AccountController>())
|
||||
, commands(&this->emplace<CommandController>())
|
||||
, highlights(&this->emplace<HighlightController>())
|
||||
, notifications(&this->emplace<NotificationController>())
|
||||
, pings(&this->emplace<PingController>())
|
||||
, ignores(&this->emplace<IgnoreController>())
|
||||
, taggedUsers(&this->emplace<TaggedUsersController>())
|
||||
, moderationActions(&this->emplace<ModerationActions>())
|
||||
, twitch2(&this->emplace<TwitchIrcServer>())
|
||||
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
|
||||
, logging(&this->emplace<Logging>())
|
||||
|
||||
{
|
||||
this->instance = this;
|
||||
|
||||
|
@ -115,9 +106,6 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
|
||||
this->initNm(paths);
|
||||
this->initPubsub();
|
||||
|
||||
this->moderationActions->items.delayedItemsChanged.connect(
|
||||
[this] { this->windows->forceLayoutChannelViews(); });
|
||||
}
|
||||
|
||||
int Application::run(QApplication &qtApp)
|
||||
|
@ -130,6 +118,13 @@ int Application::run(QApplication &qtApp)
|
|||
|
||||
getSettings()->betaUpdates.connect(
|
||||
[] { Updates::instance().checkForUpdates(); }, false);
|
||||
getSettings()->moderationActions.delayedItemsChanged.connect(
|
||||
[this] { this->windows->forceLayoutChannelViews(); });
|
||||
|
||||
getSettings()->highlightedMessages.delayedItemsChanged.connect(
|
||||
[this] { this->windows->forceLayoutChannelViews(); });
|
||||
getSettings()->highlightedUsers.delayedItemsChanged.connect(
|
||||
[this] { this->windows->forceLayoutChannelViews(); });
|
||||
|
||||
return qtApp.exec();
|
||||
}
|
||||
|
@ -156,14 +151,6 @@ void Application::initNm(Paths &paths)
|
|||
|
||||
void Application::initPubsub()
|
||||
{
|
||||
this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) {
|
||||
qDebug() << "WHISPER SENT LOL"; //
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.whisper.received.connect([](const auto &msg) {
|
||||
qDebug() << "WHISPER RECEIVED LOL"; //
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.chatCleared.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan =
|
||||
|
@ -298,6 +285,21 @@ void Application::initPubsub()
|
|||
chan->deleteMessage(msg->id);
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.pointReward.redeemed.connect([&](auto &data) {
|
||||
QString channelId;
|
||||
if (rj::getSafe(data, "channel_id", channelId))
|
||||
{
|
||||
const auto &chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(channelId);
|
||||
auto channel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||
channel->addChannelPointReward(ChannelPointReward(data));
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Couldn't find channel id of point reward";
|
||||
}
|
||||
});
|
||||
|
||||
this->twitch.pubsub->start();
|
||||
|
||||
auto RequestModerationActions = [=]() {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include <QApplication>
|
||||
#include <memory>
|
||||
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "singletons/NativeMessaging.hpp"
|
||||
|
||||
|
@ -12,13 +13,8 @@ class TwitchIrcServer;
|
|||
class PubSub;
|
||||
|
||||
class CommandController;
|
||||
class HighlightController;
|
||||
class IgnoreController;
|
||||
class TaggedUsersController;
|
||||
class AccountController;
|
||||
class ModerationActions;
|
||||
class NotificationController;
|
||||
class PingController;
|
||||
|
||||
class Theme;
|
||||
class WindowManager;
|
||||
|
@ -58,12 +54,7 @@ public:
|
|||
|
||||
AccountController *const accounts{};
|
||||
CommandController *const commands{};
|
||||
HighlightController *const highlights{};
|
||||
NotificationController *const notifications{};
|
||||
PingController *const pings{};
|
||||
IgnoreController *const ignores{};
|
||||
TaggedUsersController *const taggedUsers{};
|
||||
ModerationActions *const moderationActions{};
|
||||
TwitchIrcServer *const twitch2{};
|
||||
ChatterinoBadges *const chatterinoBadges{};
|
||||
|
||||
|
|
|
@ -159,21 +159,6 @@ void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier)
|
|||
this->messages.backgrounds.regular = getColor(0, sat, 1);
|
||||
this->messages.backgrounds.alternate = getColor(0, sat, 0.96);
|
||||
|
||||
if (isLight_)
|
||||
{
|
||||
this->messages.backgrounds.highlighted =
|
||||
blendColors(themeColor, this->messages.backgrounds.regular, 0.8);
|
||||
}
|
||||
else
|
||||
{
|
||||
// REMOVED
|
||||
// this->messages.backgrounds.highlighted =
|
||||
// QColor(getSettings()->highlightColor);
|
||||
}
|
||||
|
||||
this->messages.backgrounds.subscription =
|
||||
blendColors(QColor("#C466FF"), this->messages.backgrounds.regular, 0.7);
|
||||
|
||||
// this->messages.backgrounds.resub
|
||||
// this->messages.backgrounds.whisper
|
||||
this->messages.disabled = getColor(0, sat, 1, 0.6);
|
||||
|
|
|
@ -64,8 +64,6 @@ public:
|
|||
struct {
|
||||
QColor regular;
|
||||
QColor alternate;
|
||||
QColor highlighted;
|
||||
QColor subscription;
|
||||
// QColor whisper;
|
||||
} backgrounds;
|
||||
|
||||
|
|
|
@ -154,11 +154,6 @@ namespace {
|
|||
toBeRemoved << info.absoluteFilePath();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &&path : toBeRemoved)
|
||||
{
|
||||
qDebug() << path << QFile(path).remove();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ Resources2::Resources2()
|
|||
this->error = QPixmap(":/error.png");
|
||||
this->icon = QPixmap(":/icon.png");
|
||||
this->pajaDank = QPixmap(":/pajaDank.png");
|
||||
this->scrolling.downScroll = QPixmap(":/scrolling/downScroll.png");
|
||||
this->scrolling.neutralScroll = QPixmap(":/scrolling/neutralScroll.png");
|
||||
this->scrolling.upScroll = QPixmap(":/scrolling/upScroll.png");
|
||||
this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png");
|
||||
this->split.down = QPixmap(":/split/down.png");
|
||||
this->split.left = QPixmap(":/split/left.png");
|
||||
|
@ -50,4 +53,4 @@ Resources2::Resources2()
|
|||
this->twitch.vip = QPixmap(":/twitch/vip.png");
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino
|
|
@ -1,5 +1,4 @@
|
|||
#include <QPixmap>
|
||||
|
||||
#include "common/Singleton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -39,6 +38,11 @@ public:
|
|||
QPixmap error;
|
||||
QPixmap icon;
|
||||
QPixmap pajaDank;
|
||||
struct {
|
||||
QPixmap downScroll;
|
||||
QPixmap neutralScroll;
|
||||
QPixmap upScroll;
|
||||
} scrolling;
|
||||
struct {
|
||||
QPixmap aboutlogo;
|
||||
} settings;
|
||||
|
@ -65,4 +69,4 @@ public:
|
|||
} twitch;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino
|
|
@ -34,3 +34,4 @@ QStringAlias(Url);
|
|||
QStringAlias(Tooltip);
|
||||
QStringAlias(EmoteId);
|
||||
QStringAlias(EmoteName);
|
||||
QStringAlias(EmoteAuthor);
|
||||
|
|
|
@ -59,6 +59,11 @@ bool Channel::isEmpty() const
|
|||
return this->name_.isEmpty();
|
||||
}
|
||||
|
||||
bool Channel::hasMessages() const
|
||||
{
|
||||
return !this->messages_.empty();
|
||||
}
|
||||
|
||||
LimitedQueueSnapshot<MessagePtr> Channel::getMessageSnapshot()
|
||||
{
|
||||
return this->messages_.getSnapshot();
|
||||
|
@ -143,9 +148,8 @@ void Channel::addOrReplaceTimeout(MessagePtr message)
|
|||
|
||||
int count = s->count + 1;
|
||||
|
||||
MessageBuilder replacement(systemMessage,
|
||||
message->searchText + QString(" (") +
|
||||
QString::number(count) + " times)");
|
||||
MessageBuilder replacement(timeoutMessage, message->searchText,
|
||||
count);
|
||||
|
||||
replacement->timeoutUser = message->timeoutUser;
|
||||
replacement->count = count;
|
||||
|
|
|
@ -75,6 +75,8 @@ public:
|
|||
void deleteMessage(QString messageID);
|
||||
void clearMessages();
|
||||
|
||||
bool hasMessages() const;
|
||||
|
||||
QStringList modList;
|
||||
|
||||
// CHANNEL INFO
|
||||
|
|
|
@ -127,7 +127,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
|
|||
TaggedString::Type::Username);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!getSettings()->userCompletionOnlyWithAt)
|
||||
{
|
||||
for (const auto &name :
|
||||
usernames->subrange(Prefix(usernamePrefix)))
|
||||
|
|
|
@ -50,7 +50,7 @@ Env::Env()
|
|||
: recentMessagesApiUrl(
|
||||
readStringEnv("CHATTERINO2_RECENT_MESSAGES_URL",
|
||||
"https://recent-messages.robotty.de/api/v2/"
|
||||
"recent-messages/%1?clearchatToNotice=true"))
|
||||
"recent-messages/%1"))
|
||||
, linkResolverUrl(readStringEnv(
|
||||
"CHATTERINO2_LINK_RESOLVER_URL",
|
||||
"https://braize.pajlada.com/chatterino/link_resolver/%1"))
|
||||
|
|
62
src/common/IrcColors.hpp
Normal file
62
src/common/IrcColors.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// Colors taken from https://modern.ircdocs.horse/formatting.html
|
||||
static QMap<int, QColor> IRC_COLORS = {
|
||||
{0, QColor("white")}, {1, QColor("black")},
|
||||
{2, QColor("blue")}, {3, QColor("green")},
|
||||
{4, QColor("red")}, {5, QColor("brown")},
|
||||
{6, QColor("purple")}, {7, QColor("orange")},
|
||||
{8, QColor("yellow")}, {9, QColor("lightgreen")},
|
||||
{10, QColor("cyan")}, {11, QColor("lightcyan")},
|
||||
{12, QColor("lightblue")}, {13, QColor("pink")},
|
||||
{14, QColor("gray")}, {15, QColor("lightgray")},
|
||||
{16, QColor("#470000")}, {17, QColor("#472100")},
|
||||
{18, QColor("#474700")}, {19, QColor("#324700")},
|
||||
{20, QColor("#004700")}, {21, QColor("#00472c")},
|
||||
{22, QColor("#004747")}, {23, QColor("#002747")},
|
||||
{24, QColor("#000047")}, {25, QColor("#2e0047")},
|
||||
{26, QColor("#470047")}, {27, QColor("#47002a")},
|
||||
{28, QColor("#740000")}, {29, QColor("#743a00")},
|
||||
{30, QColor("#747400")}, {31, QColor("#517400")},
|
||||
{32, QColor("#007400")}, {33, QColor("#007449")},
|
||||
{34, QColor("#007474")}, {35, QColor("#004074")},
|
||||
{36, QColor("#000074")}, {37, QColor("#4b0074")},
|
||||
{38, QColor("#740074")}, {39, QColor("#740045")},
|
||||
{40, QColor("#b50000")}, {41, QColor("#b56300")},
|
||||
{42, QColor("#b5b500")}, {43, QColor("#7db500")},
|
||||
{44, QColor("#00b500")}, {45, QColor("#00b571")},
|
||||
{46, QColor("#00b5b5")}, {47, QColor("#0063b5")},
|
||||
{48, QColor("#0000b5")}, {49, QColor("#7500b5")},
|
||||
{50, QColor("#b500b5")}, {51, QColor("#b5006b")},
|
||||
{52, QColor("#ff0000")}, {53, QColor("#ff8c00")},
|
||||
{54, QColor("#ffff00")}, {55, QColor("#b2ff00")},
|
||||
{56, QColor("#00ff00")}, {57, QColor("#00ffa0")},
|
||||
{58, QColor("#00ffff")}, {59, QColor("#008cff")},
|
||||
{60, QColor("#0000ff")}, {61, QColor("#a500ff")},
|
||||
{62, QColor("#ff00ff")}, {63, QColor("#ff0098")},
|
||||
{64, QColor("#ff5959")}, {65, QColor("#ffb459")},
|
||||
{66, QColor("#ffff71")}, {67, QColor("#cfff60")},
|
||||
{68, QColor("#6fff6f")}, {69, QColor("#65ffc9")},
|
||||
{70, QColor("#6dffff")}, {71, QColor("#59b4ff")},
|
||||
{72, QColor("#5959ff")}, {73, QColor("#c459ff")},
|
||||
{74, QColor("#ff66ff")}, {75, QColor("#ff59bc")},
|
||||
{76, QColor("#ff9c9c")}, {77, QColor("#ffd39c")},
|
||||
{78, QColor("#ffff9c")}, {79, QColor("#e2ff9c")},
|
||||
{80, QColor("#9cff9c")}, {81, QColor("#9cffdb")},
|
||||
{82, QColor("#9cffff")}, {83, QColor("#9cd3ff")},
|
||||
{84, QColor("#9c9cff")}, {85, QColor("#dc9cff")},
|
||||
{86, QColor("#ff9cff")}, {87, QColor("#ff94d3")},
|
||||
{88, QColor("#000000")}, {89, QColor("#131313")},
|
||||
{90, QColor("#282828")}, {91, QColor("#363636")},
|
||||
{92, QColor("#4d4d4d")}, {93, QColor("#656565")},
|
||||
{94, QColor("#818181")}, {95, QColor("#9f9f9f")},
|
||||
{96, QColor("#bcbcbc")}, {97, QColor("#e2e2e2")},
|
||||
{98, QColor("#ffffff")},
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -99,6 +99,20 @@ NetworkRequest NetworkRequest::header(const char *headerName,
|
|||
return std::move(*this);
|
||||
}
|
||||
|
||||
NetworkRequest NetworkRequest::headerList(const QStringList &headers) &&
|
||||
{
|
||||
for (const QString &header : headers)
|
||||
{
|
||||
const QStringList thisHeader = header.trimmed().split(":");
|
||||
if (thisHeader.size() == 2)
|
||||
{
|
||||
this->data->request_.setRawHeader(thisHeader[0].trimmed().toUtf8(),
|
||||
thisHeader[1].trimmed().toUtf8());
|
||||
}
|
||||
}
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
NetworkRequest NetworkRequest::timeout(int ms) &&
|
||||
{
|
||||
this->data->hasTimeout_ = true;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/NetworkCommon.hpp"
|
||||
#include "common/NetworkResult.hpp"
|
||||
|
||||
#include <QHttpMultiPart>
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -52,6 +53,7 @@ public:
|
|||
NetworkRequest header(const char *headerName, const char *value) &&;
|
||||
NetworkRequest header(const char *headerName, const QByteArray &value) &&;
|
||||
NetworkRequest header(const char *headerName, const QString &value) &&;
|
||||
NetworkRequest headerList(const QStringList &headers) &&;
|
||||
NetworkRequest timeout(int ms) &&;
|
||||
NetworkRequest concurrent() &&;
|
||||
NetworkRequest authorizeTwitchV5(const QString &clientID,
|
||||
|
|
|
@ -4,244 +4,171 @@
|
|||
#include <QTimer>
|
||||
#include <boost/noncopyable.hpp>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <shared_mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
template <typename TVectorItem>
|
||||
struct SignalVectorItemArgs {
|
||||
const TVectorItem &item;
|
||||
template <typename T>
|
||||
struct SignalVectorItemEvent {
|
||||
const T &item;
|
||||
int index;
|
||||
void *caller;
|
||||
};
|
||||
|
||||
template <typename TVectorItem>
|
||||
class ReadOnlySignalVector : boost::noncopyable
|
||||
template <typename T>
|
||||
class SignalVector : boost::noncopyable
|
||||
{
|
||||
using VecIt = typename std::vector<TVectorItem>::iterator;
|
||||
|
||||
public:
|
||||
struct Iterator
|
||||
: public std::iterator<std::input_iterator_tag, TVectorItem> {
|
||||
Iterator(VecIt &&it, std::shared_mutex &mutex)
|
||||
: it_(std::move(it))
|
||||
, lock_(mutex)
|
||||
, mutex_(mutex)
|
||||
{
|
||||
}
|
||||
pajlada::Signals::Signal<SignalVectorItemEvent<T>> itemInserted;
|
||||
pajlada::Signals::Signal<SignalVectorItemEvent<T>> itemRemoved;
|
||||
pajlada::Signals::NoArgSignal delayedItemsChanged;
|
||||
|
||||
Iterator(const Iterator &other)
|
||||
: it_(other.it_)
|
||||
, lock_(other.mutex_)
|
||||
, mutex_(other.mutex_)
|
||||
{
|
||||
}
|
||||
|
||||
Iterator &operator=(const Iterator &other)
|
||||
{
|
||||
this->lock_ = std::shared_lock(other.mutex_.get());
|
||||
this->mutex_ = other.mutex_;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
TVectorItem &operator*()
|
||||
{
|
||||
return it_.operator*();
|
||||
}
|
||||
|
||||
Iterator &operator++()
|
||||
{
|
||||
++this->it_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const Iterator &other)
|
||||
{
|
||||
return this->it_ == other.it_;
|
||||
}
|
||||
|
||||
bool operator!=(const Iterator &other)
|
||||
{
|
||||
return this->it_ != other.it_;
|
||||
}
|
||||
|
||||
auto operator-(const Iterator &other)
|
||||
{
|
||||
return this->it_ - other.it_;
|
||||
}
|
||||
|
||||
private:
|
||||
VecIt it_;
|
||||
std::shared_lock<std::shared_mutex> lock_;
|
||||
std::reference_wrapper<std::shared_mutex> mutex_;
|
||||
};
|
||||
|
||||
ReadOnlySignalVector()
|
||||
SignalVector()
|
||||
: readOnly_(new std::vector<T>())
|
||||
{
|
||||
QObject::connect(&this->itemsChangedTimer_, &QTimer::timeout,
|
||||
[this] { this->delayedItemsChanged.invoke(); });
|
||||
this->itemsChangedTimer_.setInterval(100);
|
||||
this->itemsChangedTimer_.setSingleShot(true);
|
||||
}
|
||||
virtual ~ReadOnlySignalVector() = default;
|
||||
|
||||
pajlada::Signals::Signal<SignalVectorItemArgs<TVectorItem>> itemInserted;
|
||||
pajlada::Signals::Signal<SignalVectorItemArgs<TVectorItem>> itemRemoved;
|
||||
pajlada::Signals::NoArgSignal delayedItemsChanged;
|
||||
|
||||
Iterator begin() const
|
||||
SignalVector(std::function<bool(const T &, const T &)> &&compare)
|
||||
: SignalVector()
|
||||
{
|
||||
return Iterator(
|
||||
const_cast<std::vector<TVectorItem> &>(this->vector_).begin(),
|
||||
this->mutex_);
|
||||
itemCompare_ = std::move(compare);
|
||||
}
|
||||
|
||||
Iterator end() const
|
||||
virtual bool isSorted() const
|
||||
{
|
||||
return Iterator(
|
||||
const_cast<std::vector<TVectorItem> &>(this->vector_).end(),
|
||||
this->mutex_);
|
||||
return bool(this->itemCompare_);
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
/// A read-only version of the vector which can be used concurrently.
|
||||
std::shared_ptr<const std::vector<T>> readOnly()
|
||||
{
|
||||
std::shared_lock lock(this->mutex_);
|
||||
|
||||
return this->vector_.empty();
|
||||
return this->readOnly_;
|
||||
}
|
||||
|
||||
const std::vector<TVectorItem> &getVector() const
|
||||
/// This may only be called from the GUI thread.
|
||||
///
|
||||
/// @param item
|
||||
/// Item to be inserted.
|
||||
/// @param proposedIndex
|
||||
/// Index to insert at. `-1` will append at the end.
|
||||
/// Will be ignored if the vector is sorted.
|
||||
/// @param caller
|
||||
/// Caller id which will be passed in the itemInserted and itemRemoved
|
||||
/// signals.
|
||||
int insert(const T &item, int index = -1, void *caller = nullptr)
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
return this->vector_;
|
||||
if (this->isSorted())
|
||||
{
|
||||
auto it = std::lower_bound(this->items_.begin(), this->items_.end(),
|
||||
item, this->itemCompare_);
|
||||
index = it - this->items_.begin();
|
||||
this->items_.insert(it, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index == -1)
|
||||
index = this->items_.size();
|
||||
else
|
||||
assert(index >= 0 && index <= this->items_.size());
|
||||
|
||||
this->items_.insert(this->items_.begin() + index, item);
|
||||
}
|
||||
|
||||
SignalVectorItemEvent<T> args{item, index, caller};
|
||||
this->itemInserted.invoke(args);
|
||||
this->itemsChanged_();
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
std::vector<TVectorItem> cloneVector() const
|
||||
/// This may only be called from the GUI thread.
|
||||
///
|
||||
/// @param item
|
||||
/// Item to be appended.
|
||||
/// @param caller
|
||||
/// Caller id which will be passed in the itemInserted and itemRemoved
|
||||
/// signals.
|
||||
int append(const T &item, void *caller = nullptr)
|
||||
{
|
||||
std::shared_lock lock(this->mutex_);
|
||||
|
||||
return this->vector_;
|
||||
return this->insert(item, -1, caller);
|
||||
}
|
||||
|
||||
void invokeDelayedItemsChanged()
|
||||
void removeAt(int index, void *caller = nullptr)
|
||||
{
|
||||
assertInGuiThread();
|
||||
assert(index >= 0 && index < int(this->items_.size()));
|
||||
|
||||
T item = this->items_[index];
|
||||
this->items_.erase(this->items_.begin() + index);
|
||||
|
||||
SignalVectorItemEvent<T> args{item, index, caller};
|
||||
this->itemRemoved.invoke(args);
|
||||
|
||||
this->itemsChanged_();
|
||||
}
|
||||
|
||||
const std::vector<T> &raw() const
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
return this->items_;
|
||||
}
|
||||
|
||||
[[deprecated]] std::vector<T> cloneVector()
|
||||
{
|
||||
return *this->readOnly();
|
||||
}
|
||||
|
||||
// mirror vector functions
|
||||
auto begin() const
|
||||
{
|
||||
assertInGuiThread();
|
||||
return this->items_.begin();
|
||||
}
|
||||
|
||||
auto end() const
|
||||
{
|
||||
assertInGuiThread();
|
||||
return this->items_.end();
|
||||
}
|
||||
|
||||
decltype(auto) operator[](size_t index)
|
||||
{
|
||||
assertInGuiThread();
|
||||
return this->items[index];
|
||||
}
|
||||
|
||||
auto empty()
|
||||
{
|
||||
assertInGuiThread();
|
||||
return this->items_.empty();
|
||||
}
|
||||
|
||||
private:
|
||||
void itemsChanged_()
|
||||
{
|
||||
// emit delayed event
|
||||
if (!this->itemsChangedTimer_.isActive())
|
||||
{
|
||||
this->itemsChangedTimer_.start();
|
||||
}
|
||||
|
||||
// update concurrent version
|
||||
this->readOnly_ = std::make_shared<const std::vector<T>>(this->items_);
|
||||
}
|
||||
|
||||
virtual bool isSorted() const = 0;
|
||||
|
||||
protected:
|
||||
std::vector<TVectorItem> vector_;
|
||||
std::vector<T> items_;
|
||||
std::shared_ptr<const std::vector<T>> readOnly_;
|
||||
QTimer itemsChangedTimer_;
|
||||
mutable std::shared_mutex mutex_;
|
||||
};
|
||||
|
||||
template <typename TVectorItem>
|
||||
class BaseSignalVector : public ReadOnlySignalVector<TVectorItem>
|
||||
{
|
||||
public:
|
||||
// returns the actual index of the inserted item
|
||||
virtual int insertItem(const TVectorItem &item, int proposedIndex = -1,
|
||||
void *caller = nullptr) = 0;
|
||||
|
||||
void removeItem(int index, void *caller = nullptr)
|
||||
{
|
||||
assertInGuiThread();
|
||||
std::unique_lock lock(this->mutex_);
|
||||
|
||||
assert(index >= 0 && index < int(this->vector_.size()));
|
||||
|
||||
TVectorItem item = this->vector_[index];
|
||||
|
||||
this->vector_.erase(this->vector_.begin() + index);
|
||||
lock.unlock(); // manual unlock
|
||||
|
||||
SignalVectorItemArgs<TVectorItem> args{item, index, caller};
|
||||
this->itemRemoved.invoke(args);
|
||||
|
||||
this->invokeDelayedItemsChanged();
|
||||
}
|
||||
|
||||
int appendItem(const TVectorItem &item, void *caller = nullptr)
|
||||
{
|
||||
return this->insertItem(item, -1, caller);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename TVectorItem>
|
||||
class UnsortedSignalVector : public BaseSignalVector<TVectorItem>
|
||||
{
|
||||
public:
|
||||
virtual int insertItem(const TVectorItem &item, int index = -1,
|
||||
void *caller = nullptr) override
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
{
|
||||
std::unique_lock lock(this->mutex_);
|
||||
if (index == -1)
|
||||
{
|
||||
index = this->vector_.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(index >= 0 && index <= this->vector_.size());
|
||||
}
|
||||
|
||||
this->vector_.insert(this->vector_.begin() + index, item);
|
||||
}
|
||||
|
||||
SignalVectorItemArgs<TVectorItem> args{item, index, caller};
|
||||
this->itemInserted.invoke(args);
|
||||
this->invokeDelayedItemsChanged();
|
||||
return index;
|
||||
}
|
||||
|
||||
virtual bool isSorted() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename TVectorItem, typename Compare>
|
||||
class SortedSignalVector : public BaseSignalVector<TVectorItem>
|
||||
{
|
||||
public:
|
||||
virtual int insertItem(const TVectorItem &item, int = -1,
|
||||
void *caller = nullptr) override
|
||||
{
|
||||
assertInGuiThread();
|
||||
int index = -1;
|
||||
|
||||
{
|
||||
std::unique_lock lock(this->mutex_);
|
||||
|
||||
auto it = std::lower_bound(this->vector_.begin(),
|
||||
this->vector_.end(), item, Compare{});
|
||||
index = it - this->vector_.begin();
|
||||
this->vector_.insert(it, item);
|
||||
}
|
||||
|
||||
SignalVectorItemArgs<TVectorItem> args{item, index, caller};
|
||||
this->itemInserted.invoke(args);
|
||||
this->invokeDelayedItemsChanged();
|
||||
return index;
|
||||
}
|
||||
|
||||
virtual bool isSorted() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::function<bool(const T &, const T &)> itemCompare_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -25,11 +25,11 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void init(BaseSignalVector<TVectorItem> *vec)
|
||||
void initialize(SignalVector<TVectorItem> *vec)
|
||||
{
|
||||
this->vector_ = vec;
|
||||
|
||||
auto insert = [this](const SignalVectorItemArgs<TVectorItem> &args) {
|
||||
auto insert = [this](const SignalVectorItemEvent<TVectorItem> &args) {
|
||||
if (args.caller == this)
|
||||
{
|
||||
return;
|
||||
|
@ -52,9 +52,9 @@ public:
|
|||
};
|
||||
|
||||
int i = 0;
|
||||
for (const TVectorItem &item : vec->getVector())
|
||||
for (const TVectorItem &item : vec->raw())
|
||||
{
|
||||
SignalVectorItemArgs<TVectorItem> args{item, i++, 0};
|
||||
SignalVectorItemEvent<TVectorItem> args{item, i++, 0};
|
||||
|
||||
insert(args);
|
||||
}
|
||||
|
@ -89,6 +89,12 @@ public:
|
|||
this->afterInit();
|
||||
}
|
||||
|
||||
SignalVectorModel<TVectorItem> *initialized(SignalVector<TVectorItem> *vec)
|
||||
{
|
||||
this->initialize(vec);
|
||||
return this;
|
||||
}
|
||||
|
||||
virtual ~SignalVectorModel()
|
||||
{
|
||||
for (Row &row : this->rows_)
|
||||
|
@ -147,12 +153,12 @@ public:
|
|||
else
|
||||
{
|
||||
int vecRow = this->getVectorIndexFromModelIndex(row);
|
||||
this->vector_->removeItem(vecRow, this);
|
||||
this->vector_->removeAt(vecRow, this);
|
||||
|
||||
assert(this->rows_[row].original);
|
||||
TVectorItem item = this->getItemFromRow(
|
||||
this->rows_[row].items, this->rows_[row].original.get());
|
||||
this->vector_->insertItem(item, vecRow, this);
|
||||
this->vector_->insert(item, vecRow, this);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -219,7 +225,7 @@ public:
|
|||
void deleteRow(int row)
|
||||
{
|
||||
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
|
||||
this->vector_->removeItem(signalVectorRow);
|
||||
this->vector_->removeAt(signalVectorRow);
|
||||
}
|
||||
|
||||
bool removeRows(int row, int count, const QModelIndex &parent) override
|
||||
|
@ -234,11 +240,71 @@ public:
|
|||
assert(row >= 0 && row < this->rows_.size());
|
||||
|
||||
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
|
||||
this->vector_->removeItem(signalVectorRow);
|
||||
this->vector_->removeAt(signalVectorRow);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList mimeTypes() const override
|
||||
{
|
||||
return {"chatterino_row_id"};
|
||||
}
|
||||
|
||||
QMimeData *mimeData(const QModelIndexList &list) const
|
||||
{
|
||||
if (list.length() == 1)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if all indices are in the same row -> single row selected
|
||||
for (auto &&x : list)
|
||||
{
|
||||
if (x.row() != list.first().row())
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto data = new QMimeData;
|
||||
data->setData("chatterino_row_id", QByteArray::number(list[0].row()));
|
||||
return data;
|
||||
}
|
||||
|
||||
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int /*row*/,
|
||||
int /*column*/, const QModelIndex &parent) override
|
||||
{
|
||||
if (data->hasFormat("chatterino_row_id") &&
|
||||
action & (Qt::DropAction::MoveAction | Qt::DropAction::CopyAction))
|
||||
{
|
||||
int from = data->data("chatterino_row_id").toInt();
|
||||
int to = parent.row();
|
||||
|
||||
if (from < 0 || from > this->vector_->raw().size() || to < 0 ||
|
||||
to > this->vector_->raw().size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (from != to)
|
||||
{
|
||||
auto item = this->vector_->raw()[from];
|
||||
this->vector_->removeAt(from);
|
||||
this->vector_->insert(item, to);
|
||||
}
|
||||
|
||||
// We return false since we remove items ourselves.
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::DropActions supportedDropActions() const override
|
||||
{
|
||||
return this->vector_->isSorted()
|
||||
? Qt::DropActions()
|
||||
: Qt::DropAction::CopyAction | Qt::DropAction::MoveAction;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void afterInit()
|
||||
{
|
||||
|
@ -326,7 +392,7 @@ protected:
|
|||
|
||||
private:
|
||||
std::vector<QMap<int, QVariant>> headerData_;
|
||||
BaseSignalVector<TVectorItem> *vector_;
|
||||
SignalVector<TVectorItem> *vector_;
|
||||
std::vector<Row> rows_;
|
||||
|
||||
int columnCount_;
|
||||
|
|
|
@ -63,6 +63,11 @@ void UsernameSet::insertPrefix(const QString &value)
|
|||
string = value;
|
||||
}
|
||||
|
||||
bool UsernameSet::contains(const QString &value) const
|
||||
{
|
||||
return this->items.count(value) == 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Range
|
||||
//
|
||||
|
|
|
@ -76,6 +76,8 @@ public:
|
|||
std::pair<Iterator, bool> insert(const QString &value);
|
||||
std::pair<Iterator, bool> insert(QString &&value);
|
||||
|
||||
bool contains(const QString &value) const;
|
||||
|
||||
private:
|
||||
void insertPrefix(const QString &string);
|
||||
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
namespace chatterino {
|
||||
|
||||
AccountController::AccountController()
|
||||
: accounts_(SharedPtrElementLess<Account>{})
|
||||
{
|
||||
this->twitch.accounts.itemInserted.connect([this](const auto &args) {
|
||||
this->accounts_.insertItem(
|
||||
std::dynamic_pointer_cast<Account>(args.item));
|
||||
this->accounts_.insert(std::dynamic_pointer_cast<Account>(args.item));
|
||||
});
|
||||
|
||||
this->twitch.accounts.itemRemoved.connect([this](const auto &args) {
|
||||
if (args.caller != this)
|
||||
{
|
||||
auto &accs = this->twitch.accounts.getVector();
|
||||
auto &accs = this->twitch.accounts.raw();
|
||||
auto it = std::find(accs.begin(), accs.end(), args.item);
|
||||
assert(it != accs.end());
|
||||
|
||||
this->accounts_.removeItem(it - accs.begin(), this);
|
||||
this->accounts_.removeAt(it - accs.begin(), this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -30,10 +30,10 @@ AccountController::AccountController()
|
|||
case ProviderId::Twitch: {
|
||||
if (args.caller != this)
|
||||
{
|
||||
auto accs = this->twitch.accounts.cloneVector();
|
||||
auto &&accs = this->twitch.accounts;
|
||||
auto it = std::find(accs.begin(), accs.end(), args.item);
|
||||
assert(it != accs.end());
|
||||
this->twitch.accounts.removeItem(it - accs.begin(), this);
|
||||
this->twitch.accounts.removeAt(it - accs.begin(), this);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -50,7 +50,7 @@ AccountModel *AccountController::createModel(QObject *parent)
|
|||
{
|
||||
AccountModel *model = new AccountModel(parent);
|
||||
|
||||
model->init(&this->accounts_);
|
||||
model->initialize(&this->accounts_);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,7 @@ public:
|
|||
TwitchAccountManager twitch;
|
||||
|
||||
private:
|
||||
SortedSignalVector<std::shared_ptr<Account>, SharedPtrElementLess<Account>>
|
||||
accounts_;
|
||||
SignalVector<std::shared_ptr<Account>> accounts_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -8,28 +8,29 @@
|
|||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
#include "providers/twitch/TwitchApi.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "util/CombinePath.hpp"
|
||||
#include "widgets/dialogs/LogsPopup.hpp"
|
||||
#include "util/Twitch.hpp"
|
||||
#include "widgets/dialogs/UserInfoPopup.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#define TWITCH_DEFAULT_COMMANDS \
|
||||
{ \
|
||||
"/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", \
|
||||
"/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \
|
||||
"/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \
|
||||
"/clear", "/subscribers", "/subscribersoff", "/followers", \
|
||||
"/followersoff", "/user" \
|
||||
#define TWITCH_DEFAULT_COMMANDS \
|
||||
{ \
|
||||
"/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", \
|
||||
"/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \
|
||||
"/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \
|
||||
"/clear", "/subscribers", "/subscribersoff", "/followers", \
|
||||
"/followersoff", "/user", "/usercard", "/follow", "/unfollow", \
|
||||
"/ignore", "/unignore" \
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
@ -212,7 +213,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
// Update the setting when the vector of commands has been updated (most
|
||||
// likely from the settings dialog)
|
||||
this->items_.delayedItemsChanged.connect([this] { //
|
||||
this->commandsSetting_->setValue(this->items_.getVector());
|
||||
this->commandsSetting_->setValue(this->items_.raw());
|
||||
});
|
||||
|
||||
// Load commands from commands.json
|
||||
|
@ -222,7 +223,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
// of commands)
|
||||
for (const auto &command : this->commandsSetting_->getValue())
|
||||
{
|
||||
this->items_.appendItem(command);
|
||||
this->items_.append(command);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,7 +235,7 @@ void CommandController::save()
|
|||
CommandModel *CommandController::createModel(QObject *parent)
|
||||
{
|
||||
CommandModel *model = new CommandModel(parent);
|
||||
model->init(&this->items_);
|
||||
model->initialize(&this->items_);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
@ -245,8 +246,6 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji);
|
||||
QStringList words = text.split(' ', QString::SkipEmptyParts);
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
|
||||
if (words.length() == 0)
|
||||
{
|
||||
return text;
|
||||
|
@ -366,18 +365,17 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
return "";
|
||||
}
|
||||
|
||||
TwitchApi::findUserId(
|
||||
target, [user, channel, target](QString userId) {
|
||||
if (userId.isEmpty())
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
return;
|
||||
}
|
||||
user->followUser(userId, [channel, target]() {
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[user, channel, target](const auto &targetUser) {
|
||||
user->followUser(targetUser.id, [channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You successfully followed " + target));
|
||||
});
|
||||
},
|
||||
[channel, target] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
});
|
||||
|
||||
return "";
|
||||
|
@ -402,63 +400,26 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
return "";
|
||||
}
|
||||
|
||||
TwitchApi::findUserId(
|
||||
target, [user, channel, target](QString userId) {
|
||||
if (userId.isEmpty())
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
return;
|
||||
}
|
||||
user->unfollowUser(userId, [channel, target]() {
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[user, channel, target](const auto &targetUser) {
|
||||
user->unfollowUser(targetUser.id, [channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You successfully unfollowed " + target));
|
||||
});
|
||||
},
|
||||
[channel, target] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"User " + target + " could not be followed!"));
|
||||
});
|
||||
|
||||
return "";
|
||||
}
|
||||
else if (commandName == "/logs")
|
||||
{
|
||||
if (words.size() < 2)
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("Usage: /logs [user] (channel)"));
|
||||
return "";
|
||||
}
|
||||
auto app = getApp();
|
||||
|
||||
auto logs = new LogsPopup();
|
||||
QString target = words.at(1);
|
||||
|
||||
if (target.at(0) == '@')
|
||||
{
|
||||
target = target.mid(1);
|
||||
}
|
||||
|
||||
logs->setTargetUserName(target);
|
||||
|
||||
std::shared_ptr<Channel> logsChannel = channel;
|
||||
|
||||
if (words.size() == 3)
|
||||
{
|
||||
QString channelName = words.at(2);
|
||||
if (words.at(2).at(0) == '#')
|
||||
{
|
||||
channelName = channelName.mid(1);
|
||||
}
|
||||
|
||||
logs->setChannelName(channelName);
|
||||
|
||||
logsChannel =
|
||||
app->twitch.server->getChannelOrEmpty(channelName);
|
||||
}
|
||||
|
||||
logs->setChannel(logsChannel);
|
||||
|
||||
logs->getLogs();
|
||||
logs->setAttribute(Qt::WA_DeleteOnClose);
|
||||
logs->show();
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"Online logs functionality has been removed. If you're a "
|
||||
"moderator, you can use the /user command"));
|
||||
return "";
|
||||
}
|
||||
else if (commandName == "/user")
|
||||
|
@ -478,8 +439,8 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
channelName.remove(0, 1);
|
||||
}
|
||||
}
|
||||
QDesktopServices::openUrl("https://www.twitch.tv/popout/" +
|
||||
channelName + "/viewercard/" + words[1]);
|
||||
openTwitchUsercard(channelName, words[1]);
|
||||
|
||||
return "";
|
||||
}
|
||||
else if (commandName == "/usercard")
|
||||
|
@ -492,7 +453,6 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
}
|
||||
auto *userPopup = new UserInfoPopup;
|
||||
userPopup->setData(words[1], channel);
|
||||
userPopup->setActionOnFocusLoss(BaseWindow::Delete);
|
||||
userPopup->move(QCursor::pos());
|
||||
userPopup->show();
|
||||
return "";
|
||||
|
|
|
@ -22,7 +22,7 @@ class CommandModel;
|
|||
class CommandController final : public Singleton
|
||||
{
|
||||
public:
|
||||
UnsortedSignalVector<Command> items_;
|
||||
SignalVector<Command> items_;
|
||||
|
||||
QString execCommand(const QString &text, std::shared_ptr<Channel> channel,
|
||||
bool dryRun);
|
||||
|
@ -39,8 +39,6 @@ private:
|
|||
QMap<QString, Command> commandsMap_;
|
||||
int maxSpaces_ = 0;
|
||||
|
||||
std::mutex mutex_;
|
||||
|
||||
std::shared_ptr<pajlada::Settings::SettingManager> sm_;
|
||||
// Because the setting manager is not initialized until the initialize
|
||||
// function is called (and not in the constructor), we have to
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include "CommandModel.hpp"
|
||||
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// commandmodel
|
||||
|
@ -20,12 +22,8 @@ Command CommandModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
|||
void CommandModel::getRowFromItem(const Command &item,
|
||||
std::vector<QStandardItem *> &row)
|
||||
{
|
||||
row[0]->setData(item.name, Qt::DisplayRole);
|
||||
row[0]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
|
||||
Qt::ItemIsEditable);
|
||||
row[1]->setData(item.func, Qt::DisplayRole);
|
||||
row[1]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
|
||||
Qt::ItemIsEditable);
|
||||
setStringItem(row[0], item.name);
|
||||
setStringItem(row[1], item.func);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -11,9 +11,9 @@ class HighlightController;
|
|||
|
||||
class HighlightBlacklistModel : public SignalVectorModel<HighlightBlacklistUser>
|
||||
{
|
||||
public:
|
||||
explicit HighlightBlacklistModel(QObject *parent);
|
||||
|
||||
public:
|
||||
enum Column {
|
||||
Pattern = 0,
|
||||
UseRegex = 1,
|
||||
|
@ -28,8 +28,6 @@ protected:
|
|||
// turns a row in the model into a vector item
|
||||
virtual void getRowFromItem(const HighlightBlacklistUser &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
|
||||
friend class HighlightController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
#include "HighlightController.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/highlights/HighlightBlacklistModel.hpp"
|
||||
#include "controllers/highlights/HighlightModel.hpp"
|
||||
#include "controllers/highlights/UserHighlightModel.hpp"
|
||||
#include "widgets/dialogs/NotificationPopup.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
HighlightController::HighlightController()
|
||||
{
|
||||
}
|
||||
|
||||
void HighlightController::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
assert(!this->initialized_);
|
||||
this->initialized_ = true;
|
||||
|
||||
for (const HighlightPhrase &phrase : this->highlightsSetting_.getValue())
|
||||
{
|
||||
this->phrases.appendItem(phrase);
|
||||
}
|
||||
|
||||
this->phrases.delayedItemsChanged.connect([this] { //
|
||||
this->highlightsSetting_.setValue(this->phrases.getVector());
|
||||
});
|
||||
|
||||
for (const HighlightBlacklistUser &blacklistedUser :
|
||||
this->blacklistSetting_.getValue())
|
||||
{
|
||||
this->blacklistedUsers.appendItem(blacklistedUser);
|
||||
}
|
||||
|
||||
this->blacklistedUsers.delayedItemsChanged.connect([this] {
|
||||
this->blacklistSetting_.setValue(this->blacklistedUsers.getVector());
|
||||
});
|
||||
|
||||
for (const HighlightPhrase &user : this->userSetting_.getValue())
|
||||
{
|
||||
this->highlightedUsers.appendItem(user);
|
||||
}
|
||||
|
||||
this->highlightedUsers.delayedItemsChanged.connect([this] { //
|
||||
this->userSetting_.setValue(this->highlightedUsers.getVector());
|
||||
});
|
||||
}
|
||||
|
||||
HighlightModel *HighlightController::createModel(QObject *parent)
|
||||
{
|
||||
HighlightModel *model = new HighlightModel(parent);
|
||||
model->init(&this->phrases);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
UserHighlightModel *HighlightController::createUserModel(QObject *parent)
|
||||
{
|
||||
auto *model = new UserHighlightModel(parent);
|
||||
model->init(&this->highlightedUsers);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
bool HighlightController::isHighlightedUser(const QString &username)
|
||||
{
|
||||
const auto &userItems = this->highlightedUsers;
|
||||
for (const auto &highlightedUser : userItems)
|
||||
{
|
||||
if (highlightedUser.isMatch(username))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
HighlightBlacklistModel *HighlightController::createBlacklistModel(
|
||||
QObject *parent)
|
||||
{
|
||||
auto *model = new HighlightBlacklistModel(parent);
|
||||
model->init(&this->blacklistedUsers);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
bool HighlightController::blacklistContains(const QString &username)
|
||||
{
|
||||
for (const auto &blacklistedUser : this->blacklistedUsers)
|
||||
{
|
||||
if (blacklistedUser.isMatch(username))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void HighlightController::addHighlight(const MessagePtr &msg)
|
||||
{
|
||||
// static NotificationPopup popup;
|
||||
|
||||
// popup.updatePosition();
|
||||
// popup.addMessage(msg);
|
||||
// popup.show();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,52 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "controllers/highlights/HighlightBlacklistUser.hpp"
|
||||
#include "controllers/highlights/HighlightPhrase.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct Message;
|
||||
using MessagePtr = std::shared_ptr<const Message>;
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
|
||||
class UserHighlightModel;
|
||||
class HighlightModel;
|
||||
class HighlightBlacklistModel;
|
||||
|
||||
class HighlightController final : public Singleton
|
||||
{
|
||||
public:
|
||||
HighlightController();
|
||||
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
UnsortedSignalVector<HighlightPhrase> phrases;
|
||||
UnsortedSignalVector<HighlightBlacklistUser> blacklistedUsers;
|
||||
UnsortedSignalVector<HighlightPhrase> highlightedUsers;
|
||||
|
||||
HighlightModel *createModel(QObject *parent);
|
||||
HighlightBlacklistModel *createBlacklistModel(QObject *parent);
|
||||
UserHighlightModel *createUserModel(QObject *parent);
|
||||
|
||||
bool isHighlightedUser(const QString &username);
|
||||
bool blacklistContains(const QString &username);
|
||||
|
||||
void addHighlight(const MessagePtr &msg);
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
|
||||
ChatterinoSetting<std::vector<HighlightPhrase>> highlightsSetting_ = {
|
||||
"/highlighting/highlights"};
|
||||
ChatterinoSetting<std::vector<HighlightBlacklistUser>> blacklistSetting_ = {
|
||||
"/highlighting/blacklist"};
|
||||
ChatterinoSetting<std::vector<HighlightPhrase>> userSetting_ = {
|
||||
"/highlighting/users"};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "Application.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -63,10 +64,10 @@ void HighlightModel::afterInit()
|
|||
usernameRow[Column::CaseSensitive]->setFlags(0);
|
||||
|
||||
QUrl selfSound = QUrl(getSettings()->selfHighlightSoundUrl.getValue());
|
||||
setFilePathItem(usernameRow[Column::SoundPath], selfSound);
|
||||
setFilePathItem(usernameRow[Column::SoundPath], selfSound, false);
|
||||
|
||||
auto selfColor = ColorProvider::instance().color(ColorType::SelfHighlight);
|
||||
setColorItem(usernameRow[Column::Color], *selfColor);
|
||||
setColorItem(usernameRow[Column::Color], *selfColor, false);
|
||||
|
||||
this->insertCustomRow(usernameRow, 0);
|
||||
|
||||
|
@ -86,12 +87,13 @@ void HighlightModel::afterInit()
|
|||
|
||||
QUrl whisperSound =
|
||||
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
||||
setFilePathItem(whisperRow[Column::SoundPath], whisperSound);
|
||||
setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
|
||||
|
||||
auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
|
||||
setColorItem(whisperRow[Column::Color], *whisperColor);
|
||||
// auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
|
||||
// setColorItem(whisperRow[Column::Color], *whisperColor, false);
|
||||
whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
|
||||
|
||||
this->insertCustomRow(whisperRow, 1);
|
||||
this->insertCustomRow(whisperRow, WHISPER_ROW);
|
||||
|
||||
// Highlight settings for subscription messages
|
||||
std::vector<QStandardItem *> subRow = this->createRow();
|
||||
|
@ -107,12 +109,39 @@ void HighlightModel::afterInit()
|
|||
subRow[Column::CaseSensitive]->setFlags(0);
|
||||
|
||||
QUrl subSound = QUrl(getSettings()->subHighlightSoundUrl.getValue());
|
||||
setFilePathItem(subRow[Column::SoundPath], subSound);
|
||||
setFilePathItem(subRow[Column::SoundPath], subSound, false);
|
||||
|
||||
auto subColor = ColorProvider::instance().color(ColorType::Subscription);
|
||||
setColorItem(subRow[Column::Color], *subColor);
|
||||
setColorItem(subRow[Column::Color], *subColor, false);
|
||||
|
||||
this->insertCustomRow(subRow, 2);
|
||||
|
||||
// Highlight settings for redeemed highlight messages
|
||||
std::vector<QStandardItem *> redeemedRow = this->createRow();
|
||||
setBoolItem(redeemedRow[Column::Pattern],
|
||||
getSettings()->enableRedeemedHighlight.getValue(), true, false);
|
||||
redeemedRow[Column::Pattern]->setData(
|
||||
"Highlights redeemed with Channel Points", Qt::DisplayRole);
|
||||
// setBoolItem(redeemedRow[Column::FlashTaskbar],
|
||||
// getSettings()->enableRedeemedHighlightTaskbar.getValue(), true,
|
||||
// false);
|
||||
// setBoolItem(redeemedRow[Column::PlaySound],
|
||||
// getSettings()->enableRedeemedHighlightSound.getValue(), true,
|
||||
// false);
|
||||
redeemedRow[Column::FlashTaskbar]->setFlags(0);
|
||||
redeemedRow[Column::PlaySound]->setFlags(0);
|
||||
redeemedRow[Column::UseRegex]->setFlags(0);
|
||||
redeemedRow[Column::CaseSensitive]->setFlags(0);
|
||||
|
||||
QUrl RedeemedSound =
|
||||
QUrl(getSettings()->redeemedHighlightSoundUrl.getValue());
|
||||
setFilePathItem(redeemedRow[Column::SoundPath], RedeemedSound, false);
|
||||
|
||||
auto RedeemedColor =
|
||||
ColorProvider::instance().color(ColorType::RedeemedHighlight);
|
||||
setColorItem(redeemedRow[Column::Color], *RedeemedColor, false);
|
||||
|
||||
this->insertCustomRow(redeemedRow, 3);
|
||||
}
|
||||
|
||||
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||
|
@ -128,7 +157,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
{
|
||||
getSettings()->enableSelfHighlight.setValue(value.toBool());
|
||||
}
|
||||
else if (rowIndex == 1)
|
||||
else if (rowIndex == WHISPER_ROW)
|
||||
{
|
||||
getSettings()->enableWhisperHighlight.setValue(
|
||||
value.toBool());
|
||||
|
@ -137,6 +166,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
{
|
||||
getSettings()->enableSubHighlight.setValue(value.toBool());
|
||||
}
|
||||
else if (rowIndex == 3)
|
||||
{
|
||||
getSettings()->enableRedeemedHighlight.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -148,7 +182,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableSelfHighlightTaskbar.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == 1)
|
||||
else if (rowIndex == WHISPER_ROW)
|
||||
{
|
||||
getSettings()->enableWhisperHighlightTaskbar.setValue(
|
||||
value.toBool());
|
||||
|
@ -158,6 +192,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableSubHighlightTaskbar.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == 3)
|
||||
{
|
||||
// getSettings()->enableRedeemedHighlightTaskbar.setValue(
|
||||
// value.toBool());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -169,7 +208,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableSelfHighlightSound.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == 1)
|
||||
else if (rowIndex == WHISPER_ROW)
|
||||
{
|
||||
getSettings()->enableWhisperHighlightSound.setValue(
|
||||
value.toBool());
|
||||
|
@ -179,6 +218,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableSubHighlightSound.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == 3)
|
||||
{
|
||||
// getSettings()->enableRedeemedHighlightSound.setValue(
|
||||
// value.toBool());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -199,7 +243,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->selfHighlightSoundUrl.setValue(
|
||||
value.toString());
|
||||
}
|
||||
else if (rowIndex == 1)
|
||||
else if (rowIndex == WHISPER_ROW)
|
||||
{
|
||||
getSettings()->whisperHighlightSoundUrl.setValue(
|
||||
value.toString());
|
||||
|
@ -209,6 +253,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->subHighlightSoundUrl.setValue(
|
||||
value.toString());
|
||||
}
|
||||
else if (rowIndex == 3)
|
||||
{
|
||||
getSettings()->redeemedHighlightSoundUrl.setValue(
|
||||
value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -221,18 +270,27 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
{
|
||||
getSettings()->selfHighlightColor.setValue(colorName);
|
||||
}
|
||||
else if (rowIndex == 1)
|
||||
{
|
||||
getSettings()->whisperHighlightColor.setValue(colorName);
|
||||
}
|
||||
// else if (rowIndex == WHISPER_ROW)
|
||||
// {
|
||||
// getSettings()->whisperHighlightColor.setValue(colorName);
|
||||
// }
|
||||
else if (rowIndex == 2)
|
||||
{
|
||||
getSettings()->subHighlightColor.setValue(colorName);
|
||||
}
|
||||
else if (rowIndex == 3)
|
||||
{
|
||||
getSettings()->redeemedHighlightColor.setValue(colorName);
|
||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
||||
.updateColor(ColorType::RedeemedHighlight,
|
||||
QColor(colorName));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
getApp()->windows->forceLayoutChannelViews();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
class HighlightController;
|
||||
|
||||
class HighlightModel : public SignalVectorModel<HighlightPhrase>
|
||||
{
|
||||
public:
|
||||
explicit HighlightModel(QObject *parent);
|
||||
|
||||
public:
|
||||
// Used here, in HighlightingPage and in UserHighlightModel
|
||||
enum Column {
|
||||
Pattern = 0,
|
||||
|
@ -25,6 +23,8 @@ public:
|
|||
Color = 6
|
||||
};
|
||||
|
||||
constexpr static int WHISPER_ROW = 1;
|
||||
|
||||
protected:
|
||||
// turn a vector item into a model row
|
||||
virtual HighlightPhrase getItemFromRow(
|
||||
|
@ -40,8 +40,6 @@ protected:
|
|||
virtual void customRowSetData(const std::vector<QStandardItem *> &row,
|
||||
int column, const QVariant &value, int role,
|
||||
int rowIndex) override;
|
||||
|
||||
friend class HighlightController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127);
|
||||
QColor HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR =
|
||||
QColor(28, 126, 141, 90);
|
||||
QColor HighlightPhrase::FALLBACK_SUB_COLOR = QColor(196, 102, 255, 100);
|
||||
|
||||
bool HighlightPhrase::operator==(const HighlightPhrase &other) const
|
||||
{
|
||||
return std::tie(this->pattern_, this->hasSound_, this->hasAlert_,
|
||||
|
|
|
@ -70,6 +70,14 @@ public:
|
|||
const QUrl &getSoundUrl() const;
|
||||
const std::shared_ptr<QColor> getColor() const;
|
||||
|
||||
/*
|
||||
* XXX: Use the constexpr constructor here once we are building with
|
||||
* Qt>=5.13.
|
||||
*/
|
||||
static QColor FALLBACK_HIGHLIGHT_COLOR;
|
||||
static QColor FALLBACK_REDEEMED_HIGHLIGHT_COLOR;
|
||||
static QColor FALLBACK_SUB_COLOR;
|
||||
|
||||
private:
|
||||
QString pattern_;
|
||||
bool hasAlert_;
|
||||
|
@ -132,6 +140,8 @@ struct Deserialize<chatterino::HighlightPhrase> {
|
|||
chatterino::rj::getSafe(value, "color", encodedColor);
|
||||
|
||||
auto _color = QColor(encodedColor);
|
||||
if (!_color.isValid())
|
||||
_color = chatterino::HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR;
|
||||
|
||||
return chatterino::HighlightPhrase(_pattern, _hasAlert, _hasSound,
|
||||
_isRegex, _isCaseSensitive,
|
||||
|
|
|
@ -11,6 +11,7 @@ class HighlightController;
|
|||
|
||||
class UserHighlightModel : public SignalVectorModel<HighlightPhrase>
|
||||
{
|
||||
public:
|
||||
explicit UserHighlightModel(QObject *parent);
|
||||
|
||||
protected:
|
||||
|
@ -21,8 +22,6 @@ protected:
|
|||
|
||||
virtual void getRowFromItem(const HighlightPhrase &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
|
||||
friend class HighlightController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/ignores/IgnoreModel.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void IgnoreController::initialize(Settings &, Paths &)
|
||||
{
|
||||
assert(!this->initialized_);
|
||||
this->initialized_ = true;
|
||||
|
||||
for (const IgnorePhrase &phrase : this->ignoresSetting_.getValue())
|
||||
{
|
||||
this->phrases.appendItem(phrase);
|
||||
}
|
||||
|
||||
this->phrases.delayedItemsChanged.connect([this] { //
|
||||
this->ignoresSetting_.setValue(this->phrases.getVector());
|
||||
});
|
||||
}
|
||||
|
||||
IgnoreModel *IgnoreController::createModel(QObject *parent)
|
||||
{
|
||||
IgnoreModel *model = new IgnoreModel(parent);
|
||||
model->init(&this->phrases);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,33 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
|
||||
class IgnoreModel;
|
||||
|
||||
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
||||
|
||||
class IgnoreController final : public Singleton
|
||||
{
|
||||
public:
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
UnsortedSignalVector<IgnorePhrase> phrases;
|
||||
|
||||
IgnoreModel *createModel(QObject *parent);
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
|
||||
ChatterinoSetting<std::vector<IgnorePhrase>> ignoresSetting_ = {
|
||||
"/ignore/phrases"};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
class IgnoreController;
|
||||
|
||||
class IgnoreModel : public SignalVectorModel<IgnorePhrase>
|
||||
{
|
||||
public:
|
||||
explicit IgnoreModel(QObject *parent);
|
||||
|
||||
protected:
|
||||
|
@ -21,8 +20,6 @@ protected:
|
|||
// turns a row in the model into a vector item
|
||||
virtual void getRowFromItem(const IgnorePhrase &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
|
||||
friend class IgnoreController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -71,11 +71,11 @@ ModerationAction::ModerationAction(const QString &action)
|
|||
}
|
||||
else if (action.startsWith("/ban "))
|
||||
{
|
||||
this->image_ = Image::fromPixmap(getResources().buttons.ban);
|
||||
this->imageToLoad_ = 1;
|
||||
}
|
||||
else if (action.startsWith("/delete "))
|
||||
{
|
||||
this->image_ = Image::fromPixmap(getResources().buttons.trashCan);
|
||||
this->imageToLoad_ = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -100,6 +100,16 @@ bool ModerationAction::isImage() const
|
|||
|
||||
const boost::optional<ImagePtr> &ModerationAction::getImage() const
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
if (this->imageToLoad_ != 0)
|
||||
{
|
||||
if (this->imageToLoad_ == 1)
|
||||
this->image_ = Image::fromPixmap(getResources().buttons.ban);
|
||||
else if (this->imageToLoad_ == 2)
|
||||
this->image_ = Image::fromPixmap(getResources().buttons.trashCan);
|
||||
}
|
||||
|
||||
return this->image_;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,10 +25,11 @@ public:
|
|||
const QString &getAction() const;
|
||||
|
||||
private:
|
||||
boost::optional<ImagePtr> image_;
|
||||
mutable boost::optional<ImagePtr> image_;
|
||||
QString line1_;
|
||||
QString line2_;
|
||||
QString action_;
|
||||
int imageToLoad_{};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
class ModerationActions;
|
||||
|
||||
class ModerationActionModel : public SignalVectorModel<ModerationAction>
|
||||
{
|
||||
public:
|
||||
|
@ -25,8 +23,6 @@ protected:
|
|||
std::vector<QStandardItem *> &row) override;
|
||||
|
||||
friend class HighlightController;
|
||||
|
||||
friend class ModerationActions;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
#include "ModerationActions.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/moderationactions/ModerationActionModel.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ModerationActions::ModerationActions()
|
||||
{
|
||||
}
|
||||
|
||||
void ModerationActions::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
assert(!this->initialized_);
|
||||
this->initialized_ = true;
|
||||
|
||||
this->setting_ =
|
||||
std::make_unique<ChatterinoSetting<std::vector<ModerationAction>>>(
|
||||
"/moderation/actions");
|
||||
|
||||
for (auto &val : this->setting_->getValue())
|
||||
{
|
||||
this->items.insertItem(val);
|
||||
}
|
||||
|
||||
this->items.delayedItemsChanged.connect([this] { //
|
||||
this->setting_->setValue(this->items.getVector());
|
||||
});
|
||||
}
|
||||
|
||||
ModerationActionModel *ModerationActions::createModel(QObject *parent)
|
||||
{
|
||||
ModerationActionModel *model = new ModerationActionModel(parent);
|
||||
model->init(&this->items);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,32 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Singleton.hpp"
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "controllers/moderationactions/ModerationAction.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
|
||||
class ModerationActionModel;
|
||||
|
||||
class ModerationActions final : public Singleton
|
||||
{
|
||||
public:
|
||||
ModerationActions();
|
||||
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
UnsortedSignalVector<ModerationAction> items;
|
||||
|
||||
ModerationActionModel *createModel(QObject *parent);
|
||||
|
||||
private:
|
||||
std::unique_ptr<ChatterinoSetting<std::vector<ModerationAction>>> setting_;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -4,8 +4,8 @@
|
|||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "controllers/notifications/NotificationModel.hpp"
|
||||
#include "providers/twitch/TwitchApi.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "singletons/Toasts.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
@ -26,12 +26,11 @@ void NotificationController::initialize(Settings &settings, Paths &paths)
|
|||
this->initialized_ = true;
|
||||
for (const QString &channelName : this->twitchSetting_.getValue())
|
||||
{
|
||||
this->channelMap[Platform::Twitch].appendItem(channelName);
|
||||
this->channelMap[Platform::Twitch].append(channelName);
|
||||
}
|
||||
|
||||
this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { //
|
||||
this->twitchSetting_.setValue(
|
||||
this->channelMap[Platform::Twitch].getVector());
|
||||
this->twitchSetting_.setValue(this->channelMap[Platform::Twitch].raw());
|
||||
});
|
||||
/*
|
||||
for (const QString &channelName : this->mixerSetting_.getValue()) {
|
||||
|
@ -81,18 +80,18 @@ bool NotificationController::isChannelNotified(const QString &channelName,
|
|||
void NotificationController::addChannelNotification(const QString &channelName,
|
||||
Platform p)
|
||||
{
|
||||
channelMap[p].appendItem(channelName);
|
||||
channelMap[p].append(channelName);
|
||||
}
|
||||
|
||||
void NotificationController::removeChannelNotification(
|
||||
const QString &channelName, Platform p)
|
||||
{
|
||||
for (std::vector<int>::size_type i = 0;
|
||||
i != channelMap[p].getVector().size(); i++)
|
||||
for (std::vector<int>::size_type i = 0; i != channelMap[p].raw().size();
|
||||
i++)
|
||||
{
|
||||
if (channelMap[p].getVector()[i].toLower() == channelName.toLower())
|
||||
if (channelMap[p].raw()[i].toLower() == channelName.toLower())
|
||||
{
|
||||
channelMap[p].removeItem(i);
|
||||
channelMap[p].removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
@ -121,21 +120,21 @@ NotificationModel *NotificationController::createModel(QObject *parent,
|
|||
Platform p)
|
||||
{
|
||||
NotificationModel *model = new NotificationModel(parent);
|
||||
model->init(&this->channelMap[p]);
|
||||
model->initialize(&this->channelMap[p]);
|
||||
return model;
|
||||
}
|
||||
|
||||
void NotificationController::fetchFakeChannels()
|
||||
{
|
||||
for (std::vector<int>::size_type i = 0;
|
||||
i != channelMap[Platform::Twitch].getVector().size(); i++)
|
||||
i != channelMap[Platform::Twitch].raw().size(); i++)
|
||||
{
|
||||
auto chan = getApp()->twitch.server->getChannelOrEmpty(
|
||||
channelMap[Platform::Twitch].getVector()[i]);
|
||||
channelMap[Platform::Twitch].raw()[i]);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
getFakeTwitchChannelLiveStatus(
|
||||
channelMap[Platform::Twitch].getVector()[i]);
|
||||
channelMap[Platform::Twitch].raw()[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,68 +142,52 @@ void NotificationController::fetchFakeChannels()
|
|||
void NotificationController::getFakeTwitchChannelLiveStatus(
|
||||
const QString &channelName)
|
||||
{
|
||||
TwitchApi::findUserId(channelName, [channelName, this](QString roomID) {
|
||||
if (roomID.isEmpty())
|
||||
{
|
||||
getHelix()->getStreamByName(
|
||||
channelName,
|
||||
[channelName, this](bool live, const auto &stream) {
|
||||
qDebug() << "[TwitchChannel" << channelName
|
||||
<< "] Refreshing live status";
|
||||
|
||||
if (!live)
|
||||
{
|
||||
// Stream is offline
|
||||
this->removeFakeChannel(channelName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream is online
|
||||
auto i = std::find(fakeTwitchChannels.begin(),
|
||||
fakeTwitchChannels.end(), channelName);
|
||||
|
||||
if (i != fakeTwitchChannels.end())
|
||||
{
|
||||
// We have already pushed the live state of this stream
|
||||
// Could not find stream in fake twitch channels!
|
||||
return;
|
||||
}
|
||||
|
||||
if (Toasts::isEnabled())
|
||||
{
|
||||
getApp()->toasts->sendChannelNotification(channelName,
|
||||
Platform::Twitch);
|
||||
}
|
||||
if (getSettings()->notificationPlaySound)
|
||||
{
|
||||
getApp()->notifications->playSound();
|
||||
}
|
||||
if (getSettings()->notificationFlashTaskbar)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
|
||||
// Indicate that we have pushed notifications for this stream
|
||||
fakeTwitchChannels.push_back(channelName);
|
||||
},
|
||||
[channelName, this] {
|
||||
qDebug() << "[TwitchChannel" << channelName
|
||||
<< "] Refreshing live status (Missing ID)";
|
||||
removeFakeChannel(channelName);
|
||||
return;
|
||||
}
|
||||
qDebug() << "[TwitchChannel" << channelName
|
||||
<< "] Refreshing live status";
|
||||
|
||||
QString url("https://api.twitch.tv/kraken/streams/" + roomID);
|
||||
NetworkRequest::twitchRequest(url)
|
||||
.onSuccess([this, channelName](auto result) -> Outcome {
|
||||
rapidjson::Document document = result.parseRapidJson();
|
||||
if (!document.IsObject())
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] root is not "
|
||||
"an object";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
if (!document.HasMember("stream"))
|
||||
{
|
||||
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing "
|
||||
"stream in root";
|
||||
return Failure;
|
||||
}
|
||||
|
||||
const auto &stream = document["stream"];
|
||||
|
||||
if (!stream.IsObject())
|
||||
{
|
||||
// Stream is offline (stream is most likely null)
|
||||
// removeFakeChannel(channelName);
|
||||
return Failure;
|
||||
}
|
||||
// Stream is live
|
||||
auto i = std::find(fakeTwitchChannels.begin(),
|
||||
fakeTwitchChannels.end(), channelName);
|
||||
|
||||
if (!(i != fakeTwitchChannels.end()))
|
||||
{
|
||||
fakeTwitchChannels.push_back(channelName);
|
||||
if (Toasts::isEnabled())
|
||||
{
|
||||
getApp()->toasts->sendChannelNotification(
|
||||
channelName, Platform::Twitch);
|
||||
}
|
||||
if (getSettings()->notificationPlaySound)
|
||||
{
|
||||
getApp()->notifications->playSound();
|
||||
}
|
||||
if (getSettings()->notificationFlashTaskbar)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
}
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
this->removeFakeChannel(channelName);
|
||||
});
|
||||
}
|
||||
|
||||
void NotificationController::removeFakeChannel(const QString channelName)
|
||||
|
|
|
@ -30,9 +30,9 @@ public:
|
|||
|
||||
void playSound();
|
||||
|
||||
UnsortedSignalVector<QString> getVector(Platform p);
|
||||
SignalVector<QString> getVector(Platform p);
|
||||
|
||||
std::map<Platform, UnsortedSignalVector<QString>> channelMap;
|
||||
std::map<Platform, SignalVector<QString>> channelMap;
|
||||
|
||||
NotificationModel *createModel(QObject *parent, Platform p);
|
||||
|
||||
|
@ -43,6 +43,7 @@ private:
|
|||
void removeFakeChannel(const QString channelName);
|
||||
void getFakeTwitchChannelLiveStatus(const QString &channelName);
|
||||
|
||||
// fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for
|
||||
std::vector<QString> fakeTwitchChannels;
|
||||
QTimer *liveStatusTimer_;
|
||||
|
||||
|
|
28
src/controllers/pings/MutedChannelModel.cpp
Normal file
28
src/controllers/pings/MutedChannelModel.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#include "MutedChannelModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
MutedChannelModel::MutedChannelModel(QObject *parent)
|
||||
: SignalVectorModel<QString>(1, parent)
|
||||
{
|
||||
}
|
||||
|
||||
// turn a vector item into a model row
|
||||
QString MutedChannelModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
||||
const QString &original)
|
||||
{
|
||||
return QString(row[0]->data(Qt::DisplayRole).toString());
|
||||
}
|
||||
|
||||
// turn a model
|
||||
void MutedChannelModel::getRowFromItem(const QString &item,
|
||||
std::vector<QStandardItem *> &row)
|
||||
{
|
||||
setStringItem(row[0], item);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
class PingController;
|
||||
class MutedChannelController;
|
||||
|
||||
class PingModel : public SignalVectorModel<QString>
|
||||
class MutedChannelModel : public SignalVectorModel<QString>
|
||||
{
|
||||
explicit PingModel(QObject *parent);
|
||||
explicit MutedChannelModel(QObject *parent);
|
||||
|
||||
protected:
|
||||
// turn a vector item into a model row
|
||||
|
@ -21,8 +21,6 @@ protected:
|
|||
// turns a row in the model into a vector item
|
||||
virtual void getRowFromItem(const QString &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
|
||||
friend class PingController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,70 +0,0 @@
|
|||
#include "controllers/pings/PingController.hpp"
|
||||
#include "controllers/pings/PingModel.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void PingController::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
this->initialized_ = true;
|
||||
for (const QString &channelName : this->pingSetting_.getValue())
|
||||
{
|
||||
this->channelVector.appendItem(channelName);
|
||||
}
|
||||
|
||||
this->channelVector.delayedItemsChanged.connect([this] { //
|
||||
this->pingSetting_.setValue(this->channelVector.getVector());
|
||||
});
|
||||
}
|
||||
|
||||
PingModel *PingController::createModel(QObject *parent)
|
||||
{
|
||||
PingModel *model = new PingModel(parent);
|
||||
model->init(&this->channelVector);
|
||||
return model;
|
||||
}
|
||||
|
||||
bool PingController::isMuted(const QString &channelName)
|
||||
{
|
||||
for (const auto &channel : this->channelVector)
|
||||
{
|
||||
if (channelName.toLower() == channel.toLower())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PingController::muteChannel(const QString &channelName)
|
||||
{
|
||||
channelVector.appendItem(channelName);
|
||||
}
|
||||
|
||||
void PingController::unmuteChannel(const QString &channelName)
|
||||
{
|
||||
for (std::vector<int>::size_type i = 0;
|
||||
i != channelVector.getVector().size(); i++)
|
||||
{
|
||||
if (channelVector.getVector()[i].toLower() == channelName.toLower())
|
||||
{
|
||||
channelVector.removeItem(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PingController::toggleMuteChannel(const QString &channelName)
|
||||
{
|
||||
if (this->isMuted(channelName))
|
||||
{
|
||||
unmuteChannel(channelName);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
muteChannel(channelName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,36 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
|
||||
class PingModel;
|
||||
|
||||
class PingController final : public Singleton, private QObject
|
||||
{
|
||||
public:
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
bool isMuted(const QString &channelName);
|
||||
void muteChannel(const QString &channelName);
|
||||
void unmuteChannel(const QString &channelName);
|
||||
bool toggleMuteChannel(const QString &channelName);
|
||||
|
||||
PingModel *createModel(QObject *parent);
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
|
||||
UnsortedSignalVector<QString> channelVector;
|
||||
|
||||
ChatterinoSetting<std::vector<QString>> pingSetting_ = {"/pings/muted"};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,28 +0,0 @@
|
|||
#include "PingModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PingModel::PingModel(QObject *parent)
|
||||
: SignalVectorModel<QString>(1, parent)
|
||||
{
|
||||
}
|
||||
|
||||
// turn a vector item into a model row
|
||||
QString PingModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
||||
const QString &original)
|
||||
{
|
||||
return QString(row[0]->data(Qt::DisplayRole).toString());
|
||||
}
|
||||
|
||||
// turn a model
|
||||
void PingModel::getRowFromItem(const QString &item,
|
||||
std::vector<QStandardItem *> &row)
|
||||
{
|
||||
setStringItem(row[0], item);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,19 +0,0 @@
|
|||
#include "TaggedUsersController.hpp"
|
||||
|
||||
#include "controllers/taggedusers/TaggedUsersModel.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TaggedUsersController::TaggedUsersController()
|
||||
{
|
||||
}
|
||||
|
||||
TaggedUsersModel *TaggedUsersController::createModel(QObject *parent)
|
||||
{
|
||||
TaggedUsersModel *model = new TaggedUsersModel(parent);
|
||||
model->init(&this->users);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,22 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Singleton.hpp"
|
||||
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "controllers/taggedusers/TaggedUser.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TaggedUsersModel;
|
||||
|
||||
class TaggedUsersController final : public Singleton
|
||||
{
|
||||
public:
|
||||
TaggedUsersController();
|
||||
|
||||
SortedSignalVector<TaggedUser, std::less<TaggedUser>> users;
|
||||
|
||||
TaggedUsersModel *createModel(QObject *parent = nullptr);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
17
src/main.cpp
17
src/main.cpp
|
@ -9,6 +9,8 @@
|
|||
#include "common/Args.hpp"
|
||||
#include "common/Modes.hpp"
|
||||
#include "common/Version.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/IncognitoBrowser.hpp"
|
||||
|
@ -19,6 +21,10 @@ int main(int argc, char **argv)
|
|||
{
|
||||
QApplication a(argc, argv);
|
||||
|
||||
QCoreApplication::setApplicationName("chatterino");
|
||||
QCoreApplication::setApplicationVersion(CHATTERINO_VERSION);
|
||||
QCoreApplication::setOrganizationDomain("https://www.chatterino.com");
|
||||
|
||||
// convert char** to QStringList
|
||||
auto args = QStringList();
|
||||
std::transform(argv + 1, argv + argc, std::back_inserter(args),
|
||||
|
@ -32,10 +38,19 @@ int main(int argc, char **argv)
|
|||
}
|
||||
else if (getArgs().printVersion)
|
||||
{
|
||||
qInfo().noquote() << Version::instance().fullVersion();
|
||||
auto version = Version::instance();
|
||||
qInfo().noquote() << QString("%1 (commit %2%3)")
|
||||
.arg(version.fullVersion())
|
||||
.arg(version.commitHash())
|
||||
.arg(Modes::instance().isNightly
|
||||
? ", " + version.dateOfBuild()
|
||||
: "");
|
||||
}
|
||||
else
|
||||
{
|
||||
Helix::initialize();
|
||||
Kraken::initialize();
|
||||
|
||||
Paths *paths{};
|
||||
|
||||
try
|
||||
|
|
|
@ -41,6 +41,22 @@ namespace detail {
|
|||
getApp()->emotes->gifTimer.signal.connect(
|
||||
[this] { this->advance(); });
|
||||
}
|
||||
|
||||
auto totalLength = std::accumulate(
|
||||
this->items_.begin(), this->items_.end(), 0UL,
|
||||
[](auto init, auto &&frame) { return init + frame.duration; });
|
||||
|
||||
if (totalLength == 0)
|
||||
{
|
||||
this->durationOffset_ = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->durationOffset_ = std::min<int>(
|
||||
int(getApp()->emotes->gifTimer.position() % totalLength),
|
||||
60000);
|
||||
}
|
||||
this->processOffset();
|
||||
}
|
||||
|
||||
Frames::~Frames()
|
||||
|
@ -58,17 +74,21 @@ namespace detail {
|
|||
|
||||
void Frames::advance()
|
||||
{
|
||||
this->durationOffset_ += GIF_FRAME_LENGTH;
|
||||
this->durationOffset_ += gifFrameLength;
|
||||
this->processOffset();
|
||||
}
|
||||
|
||||
void Frames::processOffset()
|
||||
{
|
||||
if (this->items_.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
this->index_ %= this->items_.size();
|
||||
|
||||
// TODO: Figure out what this was supposed to achieve
|
||||
// if (this->index_ >= this->items_.size()) {
|
||||
// this->index_ = this->index_;
|
||||
// }
|
||||
|
||||
if (this->durationOffset_ > this->items_[this->index_].duration)
|
||||
{
|
||||
this->durationOffset_ -= this->items_[this->index_].duration;
|
||||
|
@ -219,10 +239,6 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale)
|
|||
{
|
||||
cache[url] = shared = ImagePtr(new Image(url, scale));
|
||||
}
|
||||
else
|
||||
{
|
||||
// qDebug() << "same image created multiple times:" << url.string;
|
||||
}
|
||||
|
||||
return shared;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ namespace detail {
|
|||
boost::optional<QPixmap> first() const;
|
||||
|
||||
private:
|
||||
void processOffset();
|
||||
QVector<Frame<QPixmap>> items_;
|
||||
int index_{0};
|
||||
int durationOffset_{0};
|
||||
|
|
|
@ -125,8 +125,6 @@ public:
|
|||
newChunks->at(0) = newFirstChunk;
|
||||
|
||||
this->chunks_ = newChunks;
|
||||
// qDebug() << acceptedItems.size();
|
||||
// qDebug() << this->chunks->at(0)->size();
|
||||
|
||||
if (this->chunks_->size() == 1)
|
||||
{
|
||||
|
@ -225,8 +223,13 @@ public:
|
|||
this->firstChunkOffset_, this->lastChunkEnd_);
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return this->limit_ - this->space() == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
qsizetype space()
|
||||
qsizetype space() const
|
||||
{
|
||||
size_t totalSize = 0;
|
||||
for (auto &chunk : *this->chunks_)
|
||||
|
|
|
@ -19,4 +19,9 @@ bool Link::isValid() const
|
|||
return this->type != None;
|
||||
}
|
||||
|
||||
bool Link::isUrl() const
|
||||
{
|
||||
return this->type == Url;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -28,6 +28,7 @@ public:
|
|||
QString value;
|
||||
|
||||
bool isValid() const;
|
||||
bool isUrl() const;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -35,6 +35,12 @@ SBHighlight Message::getScrollBarHighlight() const
|
|||
return SBHighlight(
|
||||
ColorProvider::instance().color(ColorType::Subscription));
|
||||
}
|
||||
else if (this->flags.has(MessageFlag::RedeemedHighlight))
|
||||
{
|
||||
return SBHighlight(
|
||||
ColorProvider::instance().color(ColorType::RedeemedHighlight),
|
||||
SBHighlight::Default, true);
|
||||
}
|
||||
return SBHighlight();
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ enum class MessageFlag : uint32_t {
|
|||
HighlightedWhisper = (1 << 17),
|
||||
Debug = (1 << 18),
|
||||
Similar = (1 << 19),
|
||||
RedeemedHighlight = (1 << 20),
|
||||
RedeemedChannelPointReward = (1 << 21),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "Application.hpp"
|
||||
#include "common/LinkParser.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
|
@ -23,6 +24,11 @@ MessagePtr makeSystemMessage(const QString &text)
|
|||
return MessageBuilder(systemMessage, text).release();
|
||||
}
|
||||
|
||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time)
|
||||
{
|
||||
return MessageBuilder(systemMessage, text, time).release();
|
||||
}
|
||||
|
||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
||||
const AutomodAction &action)
|
||||
{
|
||||
|
@ -92,10 +98,11 @@ MessageBuilder::MessageBuilder()
|
|||
{
|
||||
}
|
||||
|
||||
MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
|
||||
MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text,
|
||||
const QTime &time)
|
||||
: MessageBuilder()
|
||||
{
|
||||
this->emplace<TimestampElement>();
|
||||
this->emplace<TimestampElement>(time);
|
||||
|
||||
// check system message for links
|
||||
// (e.g. needed for sub ticket message in sub only mode)
|
||||
|
@ -119,17 +126,40 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
|
|||
this->message().searchText = text;
|
||||
}
|
||||
|
||||
MessageBuilder::MessageBuilder(TimeoutMessageTag,
|
||||
const QString &systemMessageText, int times)
|
||||
: MessageBuilder()
|
||||
{
|
||||
QString username = systemMessageText.split(" ").at(0);
|
||||
QString remainder = systemMessageText.mid(username.length() + 1);
|
||||
|
||||
QString text;
|
||||
|
||||
this->emplace<TimestampElement>();
|
||||
this->emplaceSystemTextAndUpdate(username, text)
|
||||
->setLink({Link::UserInfo, username});
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("%1 (%2 times)").arg(remainder.trimmed()).arg(times), text);
|
||||
|
||||
this->message().messageText = text;
|
||||
this->message().searchText = text;
|
||||
}
|
||||
|
||||
MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
|
||||
const QString &durationInSeconds,
|
||||
const QString &reason, bool multipleTimes)
|
||||
: MessageBuilder()
|
||||
{
|
||||
QString fullText;
|
||||
QString text;
|
||||
|
||||
text.append(username);
|
||||
this->emplace<TimestampElement>();
|
||||
this->emplaceSystemTextAndUpdate(username, fullText)
|
||||
->setLink({Link::UserInfo, username});
|
||||
|
||||
if (!durationInSeconds.isEmpty())
|
||||
{
|
||||
text.append(" has been timed out");
|
||||
text.append("has been timed out");
|
||||
|
||||
// TODO: Implement who timed the user out
|
||||
|
||||
|
@ -143,7 +173,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
|
|||
}
|
||||
else
|
||||
{
|
||||
text.append(" has been permanently banned");
|
||||
text.append("has been permanently banned");
|
||||
}
|
||||
|
||||
if (reason.length() > 0)
|
||||
|
@ -163,16 +193,18 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
|
|||
this->message().flags.set(MessageFlag::Timeout);
|
||||
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||
this->message().timeoutUser = username;
|
||||
this->emplace<TimestampElement>();
|
||||
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
this->message().messageText = text;
|
||||
this->message().searchText = text;
|
||||
|
||||
this->emplaceSystemTextAndUpdate(text, fullText);
|
||||
this->message().messageText = fullText;
|
||||
this->message().searchText = fullText;
|
||||
}
|
||||
|
||||
// XXX: This does not belong in the MessageBuilder, this should be part of the TwitchMessageBuilder
|
||||
MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
|
||||
: MessageBuilder()
|
||||
{
|
||||
auto current = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
this->emplace<TimestampElement>();
|
||||
this->message().flags.set(MessageFlag::System);
|
||||
this->message().flags.set(MessageFlag::Timeout);
|
||||
|
@ -181,48 +213,84 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
|
|||
|
||||
QString text;
|
||||
|
||||
if (action.isBan())
|
||||
if (action.target.id == current->getUserId())
|
||||
{
|
||||
if (action.reason.isEmpty())
|
||||
this->emplaceSystemTextAndUpdate("You were", text);
|
||||
if (action.isBan())
|
||||
{
|
||||
text = QString("%1 banned %2.") //
|
||||
.arg(action.source.name)
|
||||
.arg(action.target.name);
|
||||
this->emplaceSystemTextAndUpdate("banned", text);
|
||||
}
|
||||
else
|
||||
{
|
||||
text = QString("%1 banned %2: \"%3\".") //
|
||||
.arg(action.source.name)
|
||||
.arg(action.target.name)
|
||||
.arg(action.reason);
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("timed out for %1").arg(formatTime(action.duration)),
|
||||
text);
|
||||
}
|
||||
|
||||
if (!action.source.name.isEmpty())
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate("by", text);
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
action.source.name + (action.reason.isEmpty() ? "." : ":"),
|
||||
text)
|
||||
->setLink({Link::UserInfo, action.source.name});
|
||||
}
|
||||
|
||||
if (!action.reason.isEmpty())
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("\"%1\".").arg(action.reason), text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (action.reason.isEmpty())
|
||||
if (action.isBan())
|
||||
{
|
||||
text = QString("%1 timed out %2 for %3.") //
|
||||
.arg(action.source.name)
|
||||
.arg(action.target.name)
|
||||
.arg(formatTime(action.duration));
|
||||
this->emplaceSystemTextAndUpdate(action.source.name, text)
|
||||
->setLink({Link::UserInfo, action.source.name});
|
||||
this->emplaceSystemTextAndUpdate("banned", text);
|
||||
if (action.reason.isEmpty())
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(action.target.name, text)
|
||||
->setLink({Link::UserInfo, action.target.name});
|
||||
}
|
||||
else
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(action.target.name + ":", text)
|
||||
->setLink({Link::UserInfo, action.target.name});
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("\"%1\".").arg(action.reason), text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
text = QString("%1 timed out %2 for %3: \"%4\".") //
|
||||
.arg(action.source.name)
|
||||
.arg(action.target.name)
|
||||
.arg(formatTime(action.duration))
|
||||
.arg(action.reason);
|
||||
}
|
||||
this->emplaceSystemTextAndUpdate(action.source.name, text)
|
||||
->setLink({Link::UserInfo, action.source.name});
|
||||
this->emplaceSystemTextAndUpdate("timed out", text);
|
||||
this->emplaceSystemTextAndUpdate(action.target.name, text)
|
||||
->setLink({Link::UserInfo, action.target.name});
|
||||
if (action.reason.isEmpty())
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("for %1.").arg(formatTime(action.duration)), text);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("for %1: \"%2\".")
|
||||
.arg(formatTime(action.duration))
|
||||
.arg(action.reason),
|
||||
text);
|
||||
}
|
||||
|
||||
if (count > 1)
|
||||
{
|
||||
text.append(QString(" (%1 times)").arg(count));
|
||||
if (count > 1)
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
QString("(%1 times)").arg(count), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
this->message().messageText = text;
|
||||
this->message().searchText = text;
|
||||
}
|
||||
|
@ -236,14 +304,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action)
|
|||
|
||||
this->message().timeoutUser = action.target.name;
|
||||
|
||||
QString text =
|
||||
QString("%1 %2 %3.")
|
||||
.arg(action.source.name)
|
||||
.arg(QString(action.wasBan() ? "unbanned" : "untimedout"))
|
||||
.arg(action.target.name);
|
||||
QString text;
|
||||
|
||||
this->emplaceSystemTextAndUpdate(action.source.name, text)
|
||||
->setLink({Link::UserInfo, action.source.name});
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
action.wasBan() ? "unbanned" : "untimedout", text);
|
||||
this->emplaceSystemTextAndUpdate(action.target.name, text)
|
||||
->setLink({Link::UserInfo, action.target.name});
|
||||
|
||||
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
this->message().messageText = text;
|
||||
this->message().searchText = text;
|
||||
}
|
||||
|
@ -384,7 +453,8 @@ void MessageBuilder::addLink(const QString &origLink,
|
|||
LinkResolver::getLinkInfo(
|
||||
matchedLink, nullptr,
|
||||
[weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal,
|
||||
matchedLink](QString tooltipText, Link originalLink) {
|
||||
matchedLink](QString tooltipText, Link originalLink,
|
||||
ImagePtr thumbnail) {
|
||||
auto shared = weakMessage.lock();
|
||||
if (!shared)
|
||||
{
|
||||
|
@ -401,7 +471,21 @@ void MessageBuilder::addLink(const QString &origLink,
|
|||
linkMELowercase->setLink(originalLink)->updateLink();
|
||||
linkMEOriginal->setLink(originalLink)->updateLink();
|
||||
}
|
||||
linkMELowercase->setThumbnail(thumbnail);
|
||||
linkMELowercase->setThumbnailType(
|
||||
MessageElement::ThumbnailType::Link_Thumbnail);
|
||||
linkMEOriginal->setThumbnail(thumbnail);
|
||||
linkMEOriginal->setThumbnailType(
|
||||
MessageElement::ThumbnailType::Link_Thumbnail);
|
||||
});
|
||||
}
|
||||
|
||||
TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text,
|
||||
QString &toUpdate)
|
||||
{
|
||||
toUpdate.append(text + " ");
|
||||
return this->emplace<TextElement>(text, MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -22,6 +22,7 @@ const SystemMessageTag systemMessage{};
|
|||
const TimeoutMessageTag timeoutMessage{};
|
||||
|
||||
MessagePtr makeSystemMessage(const QString &text);
|
||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
||||
const AutomodAction &action);
|
||||
|
||||
|
@ -31,14 +32,17 @@ struct MessageParseArgs {
|
|||
bool isSentWhisper = false;
|
||||
bool trimSubscriberUsername = false;
|
||||
bool isStaffOrBroadcaster = false;
|
||||
QString channelPointRewardId = "";
|
||||
};
|
||||
|
||||
class MessageBuilder
|
||||
|
||||
{
|
||||
public:
|
||||
MessageBuilder();
|
||||
MessageBuilder(SystemMessageTag, const QString &text);
|
||||
MessageBuilder(SystemMessageTag, const QString &text,
|
||||
const QTime &time = QTime::currentTime());
|
||||
MessageBuilder(TimeoutMessageTag, const QString &systemMessageText,
|
||||
int times);
|
||||
MessageBuilder(TimeoutMessageTag, const QString &username,
|
||||
const QString &durationInSeconds, const QString &reason,
|
||||
bool multipleTimes);
|
||||
|
@ -68,6 +72,13 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
// Helper method that emplaces some text stylized as system text
|
||||
// and then appends that text to the QString parameter "toUpdate".
|
||||
// Returns the TextElement that was emplaced.
|
||||
TextElement *emplaceSystemTextAndUpdate(const QString &text,
|
||||
QString &toUpdate);
|
||||
|
||||
std::shared_ptr<Message> message_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#include "messages/MessageElement.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/moderationactions/ModerationActions.hpp"
|
||||
#include "common/IrcColors.hpp"
|
||||
#include "debug/Benchmark.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||
|
@ -12,6 +12,14 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
QRegularExpression IRC_COLOR_PARSE_REGEX(
|
||||
"(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)",
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
|
||||
} // namespace
|
||||
|
||||
MessageElement::MessageElement(MessageElementFlags flags)
|
||||
: flags_(flags)
|
||||
{
|
||||
|
@ -41,6 +49,18 @@ MessageElement *MessageElement::setTooltip(const QString &tooltip)
|
|||
return this;
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail)
|
||||
{
|
||||
this->thumbnail_ = thumbnail;
|
||||
return this;
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::setThumbnailType(const ThumbnailType type)
|
||||
{
|
||||
this->thumbnailType_ = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::setTrailingSpace(bool value)
|
||||
{
|
||||
this->trailingSpace = value;
|
||||
|
@ -52,6 +72,16 @@ const QString &MessageElement::getTooltip() const
|
|||
return this->tooltip_;
|
||||
}
|
||||
|
||||
const ImagePtr &MessageElement::getThumbnail() const
|
||||
{
|
||||
return this->thumbnail_;
|
||||
}
|
||||
|
||||
const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
|
||||
{
|
||||
return this->thumbnailType_;
|
||||
}
|
||||
|
||||
const Link &MessageElement::getLink() const
|
||||
{
|
||||
return this->link_;
|
||||
|
@ -165,21 +195,6 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement(
|
|||
return new ImageLayoutElement(*this, image, size);
|
||||
}
|
||||
|
||||
// MOD BADGE
|
||||
ModBadgeElement::ModBadgeElement(const EmotePtr &data,
|
||||
MessageElementFlags flags_)
|
||||
: EmoteElement(data, flags_)
|
||||
{
|
||||
}
|
||||
|
||||
MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
static const QColor modBadgeBackgroundColor("#34AE0A");
|
||||
return new ImageWithBackgroundLayoutElement(*this, image, size,
|
||||
modBadgeBackgroundColor);
|
||||
}
|
||||
|
||||
// BADGE
|
||||
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
|
||||
: MessageElement(flags)
|
||||
|
@ -201,8 +216,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto size = QSize(int(container.getScale() * image->width()),
|
||||
int(container.getScale() * image->height()));
|
||||
|
||||
container.addElement((new ImageLayoutElement(*this, image, size))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(this->makeImageLayoutElement(image, size));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,6 +225,34 @@ EmotePtr BadgeElement::getEmote() const
|
|||
return this->emote_;
|
||||
}
|
||||
|
||||
MessageLayoutElement *BadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto element =
|
||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
// MOD BADGE
|
||||
ModBadgeElement::ModBadgeElement(const EmotePtr &data,
|
||||
MessageElementFlags flags_)
|
||||
: BadgeElement(data, flags_)
|
||||
{
|
||||
}
|
||||
|
||||
MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
static const QColor modBadgeBackgroundColor("#34AE0A");
|
||||
|
||||
auto element = (new ImageWithBackgroundLayoutElement(
|
||||
*this, image, size, modBadgeBackgroundColor))
|
||||
->setLink(this->getLink());
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
// TEXT
|
||||
TextElement::TextElement(const QString &text, MessageElementFlags flags,
|
||||
const MessageColor &color, FontStyle style)
|
||||
|
@ -319,10 +361,9 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
if (isSurrogate)
|
||||
i++;
|
||||
}
|
||||
|
||||
container.addElement(getTextLayoutElement(
|
||||
//add the final piece of wrapped text
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
text.mid(wordStart), width, this->hasTrailingSpace()));
|
||||
container.breakLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -374,8 +415,8 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
|
|||
{
|
||||
QSize size(int(container.getScale() * 16),
|
||||
int(container.getScale() * 16));
|
||||
|
||||
for (const auto &action : getApp()->moderationActions->items)
|
||||
auto actions = getCSettings().moderationActions.readOnly();
|
||||
for (const auto &action : *actions)
|
||||
{
|
||||
if (auto image = action.getImage())
|
||||
{
|
||||
|
@ -395,4 +436,283 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
}
|
||||
|
||||
// TEXT
|
||||
// IrcTextElement gets its color from the color code in the message, and can change from character to character.
|
||||
// This differs from the TextElement
|
||||
IrcTextElement::IrcTextElement(const QString &fullText,
|
||||
MessageElementFlags flags, FontStyle style)
|
||||
: MessageElement(flags)
|
||||
, style_(style)
|
||||
{
|
||||
assert(IRC_COLOR_PARSE_REGEX.isValid());
|
||||
|
||||
// Default pen colors. -1 = default theme colors
|
||||
int fg = -1, bg = -1;
|
||||
|
||||
// Split up the message in words (space separated)
|
||||
// Each word contains one or more colored segments.
|
||||
// The color of that segment is "global", as in it can be decided by the word before it.
|
||||
for (const auto &text : fullText.split(' '))
|
||||
{
|
||||
std::vector<Segment> segments;
|
||||
|
||||
int pos = 0;
|
||||
int lastPos = 0;
|
||||
|
||||
auto i = IRC_COLOR_PARSE_REGEX.globalMatch(text);
|
||||
|
||||
while (i.hasNext())
|
||||
{
|
||||
auto match = i.next();
|
||||
|
||||
if (lastPos != match.capturedStart() && match.capturedStart() != 0)
|
||||
{
|
||||
auto seg = Segment{};
|
||||
seg.text = text.mid(lastPos, match.capturedStart() - lastPos);
|
||||
seg.fg = fg;
|
||||
seg.bg = bg;
|
||||
segments.emplace_back(seg);
|
||||
lastPos = match.capturedStart() + match.capturedLength();
|
||||
}
|
||||
if (!match.captured(1).isEmpty())
|
||||
{
|
||||
fg = -1;
|
||||
bg = -1;
|
||||
}
|
||||
|
||||
if (!match.captured(2).isEmpty())
|
||||
{
|
||||
fg = match.captured(2).toInt(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
fg = -1;
|
||||
}
|
||||
if (!match.captured(4).isEmpty())
|
||||
{
|
||||
bg = match.captured(4).toInt(nullptr);
|
||||
}
|
||||
else if (fg == -1)
|
||||
{
|
||||
bg = -1;
|
||||
}
|
||||
|
||||
lastPos = match.capturedStart() + match.capturedLength();
|
||||
}
|
||||
|
||||
auto seg = Segment{};
|
||||
seg.text = text.mid(lastPos);
|
||||
seg.fg = fg;
|
||||
seg.bg = bg;
|
||||
segments.emplace_back(seg);
|
||||
|
||||
QString n(text);
|
||||
|
||||
n.replace(IRC_COLOR_PARSE_REGEX, "");
|
||||
|
||||
Word w{
|
||||
n,
|
||||
-1,
|
||||
segments,
|
||||
};
|
||||
this->words_.emplace_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
void IrcTextElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
MessageColor defaultColorType = MessageColor::Text;
|
||||
auto defaultColor = defaultColorType.getColor(*app->themes);
|
||||
if (flags.hasAny(this->getFlags()))
|
||||
{
|
||||
QFontMetrics metrics =
|
||||
app->fonts->getFontMetrics(this->style_, container.getScale());
|
||||
|
||||
for (auto &word : this->words_)
|
||||
{
|
||||
auto getTextLayoutElement = [&](QString text,
|
||||
std::vector<Segment> segments,
|
||||
int width, bool hasTrailingSpace) {
|
||||
std::vector<PajSegment> xd{};
|
||||
|
||||
for (const auto &segment : segments)
|
||||
{
|
||||
QColor color = defaultColor;
|
||||
if (segment.fg >= 0 && segment.fg <= 98)
|
||||
{
|
||||
color = IRC_COLORS[segment.fg];
|
||||
}
|
||||
app->themes->normalizeColor(color);
|
||||
xd.emplace_back(PajSegment{segment.text, color});
|
||||
}
|
||||
|
||||
auto e = (new MultiColorTextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), xd,
|
||||
this->style_, container.getScale()))
|
||||
->setLink(this->getLink());
|
||||
e->setTrailingSpace(true);
|
||||
e->setText(text);
|
||||
|
||||
// If URL link was changed,
|
||||
// Should update it in MessageLayoutElement too!
|
||||
if (this->getLink().type == Link::Url)
|
||||
{
|
||||
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
// fourtf: add again
|
||||
// if (word.width == -1) {
|
||||
word.width = metrics.width(word.text);
|
||||
// }
|
||||
|
||||
// see if the text fits in the current line
|
||||
if (container.fitsInLine(word.width))
|
||||
{
|
||||
container.addElementNoLineBreak(
|
||||
getTextLayoutElement(word.text, word.segments, word.width,
|
||||
this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
|
||||
// see if the text fits in the next line
|
||||
if (!container.atStartOfLine())
|
||||
{
|
||||
container.breakLine();
|
||||
|
||||
if (container.fitsInLine(word.width))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
word.text, word.segments, word.width,
|
||||
this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// we done goofed, we need to wrap the text
|
||||
QString text = word.text;
|
||||
std::vector<Segment> segments = word.segments;
|
||||
int textLength = text.length();
|
||||
int wordStart = 0;
|
||||
int width = 0;
|
||||
|
||||
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
||||
|
||||
// XXX(pajlada): NOT TESTED
|
||||
for (int i = 0; i < textLength; i++) //
|
||||
{
|
||||
auto isSurrogate = text.size() > i + 1 &&
|
||||
QChar::isHighSurrogate(text[i].unicode());
|
||||
|
||||
auto charWidth = isSurrogate ? metrics.width(text.mid(i, 2))
|
||||
: metrics.width(text[i]);
|
||||
|
||||
if (!container.fitsInLine(width + charWidth))
|
||||
{
|
||||
std::vector<Segment> pieceSegments;
|
||||
int charactersLeft = i - wordStart;
|
||||
assert(charactersLeft > 0);
|
||||
for (auto segmentIt = segments.begin();
|
||||
segmentIt != segments.end();)
|
||||
{
|
||||
assert(charactersLeft > 0);
|
||||
auto &segment = *segmentIt;
|
||||
if (charactersLeft >= segment.text.length())
|
||||
{
|
||||
// Entire segment fits in this piece
|
||||
pieceSegments.push_back(segment);
|
||||
charactersLeft -= segment.text.length();
|
||||
segmentIt = segments.erase(segmentIt);
|
||||
|
||||
assert(charactersLeft >= 0);
|
||||
|
||||
if (charactersLeft == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only part of the segment fits in this piece
|
||||
// We create a new segment with the characters that fit, and modify the segment we checked to only contain the characters we didn't consume
|
||||
Segment segmentThatFitsInPiece{
|
||||
segment.text.left(charactersLeft), segment.fg,
|
||||
segment.bg};
|
||||
pieceSegments.emplace_back(segmentThatFitsInPiece);
|
||||
segment.text = segment.text.mid(charactersLeft);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
container.addElementNoLineBreak(
|
||||
getTextLayoutElement(text.mid(wordStart, i - wordStart),
|
||||
pieceSegments, width, false));
|
||||
container.breakLine();
|
||||
|
||||
wordStart = i;
|
||||
width = charWidth;
|
||||
|
||||
if (isSurrogate)
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
width += charWidth;
|
||||
|
||||
if (isSurrogate)
|
||||
i++;
|
||||
}
|
||||
|
||||
// Add last remaining text & segments
|
||||
container.addElementNoLineBreak(
|
||||
getTextLayoutElement(text.mid(wordStart), segments, width,
|
||||
this->hasTrailingSpace()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LinebreakElement::LinebreakElement(MessageElementFlags flags)
|
||||
: MessageElement(flags)
|
||||
{
|
||||
}
|
||||
|
||||
void LinebreakElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
if (flags.hasAny(this->getFlags()))
|
||||
{
|
||||
container.breakLine();
|
||||
}
|
||||
}
|
||||
|
||||
ScalingImageElement::ScalingImageElement(ImageSet images,
|
||||
MessageElementFlags flags)
|
||||
: MessageElement(flags)
|
||||
, images_(images)
|
||||
{
|
||||
}
|
||||
|
||||
void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
if (flags.hasAny(this->getFlags()))
|
||||
{
|
||||
const auto &image =
|
||||
this->images_.getImageOrLoaded(container.getScale());
|
||||
if (image->isEmpty())
|
||||
return;
|
||||
|
||||
auto size = QSize(image->width() * container.getScale(),
|
||||
image->height() * container.getScale());
|
||||
|
||||
container.addElement((new ImageLayoutElement(*this, image, size))
|
||||
->setLink(this->getLink()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "messages/Link.hpp"
|
||||
#include "messages/MessageColor.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
#include "src/messages/ImageSet.hpp"
|
||||
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
|
@ -39,6 +40,10 @@ enum class MessageElementFlag {
|
|||
BttvEmoteImage = (1 << 6),
|
||||
BttvEmoteText = (1 << 7),
|
||||
BttvEmote = BttvEmoteImage | BttvEmoteText,
|
||||
|
||||
ChannelPointReward = (1 << 8),
|
||||
ChannelPointRewardImage = ChannelPointReward | TwitchEmoteImage,
|
||||
|
||||
FfzEmoteImage = (1 << 10),
|
||||
FfzEmoteText = (1 << 11),
|
||||
FfzEmote = FfzEmoteImage | FfzEmoteText,
|
||||
|
@ -122,14 +127,23 @@ public:
|
|||
Update_Images = 4,
|
||||
Update_All = Update_Text | Update_Emotes | Update_Images
|
||||
};
|
||||
enum ThumbnailType : char {
|
||||
Link_Thumbnail = 1,
|
||||
};
|
||||
|
||||
virtual ~MessageElement();
|
||||
|
||||
MessageElement *setLink(const Link &link);
|
||||
MessageElement *setText(const QString &text);
|
||||
MessageElement *setTooltip(const QString &tooltip);
|
||||
MessageElement *setThumbnailType(const ThumbnailType type);
|
||||
MessageElement *setThumbnail(const ImagePtr &thumbnail);
|
||||
|
||||
MessageElement *setTrailingSpace(bool value);
|
||||
const QString &getTooltip() const;
|
||||
const ImagePtr &getThumbnail() const;
|
||||
const ThumbnailType &getThumbnailType() const;
|
||||
|
||||
const Link &getLink() const;
|
||||
bool hasTrailingSpace() const;
|
||||
MessageElementFlags getFlags() const;
|
||||
|
@ -148,6 +162,8 @@ private:
|
|||
QString text_;
|
||||
Link link_;
|
||||
QString tooltip_;
|
||||
ImagePtr thumbnail_;
|
||||
ThumbnailType thumbnailType_;
|
||||
MessageElementFlags flags_;
|
||||
};
|
||||
|
||||
|
@ -223,17 +239,6 @@ private:
|
|||
EmotePtr emote_;
|
||||
};
|
||||
|
||||
// Behaves like an emote element, except it creates a different image layout element that draws the mod badge background
|
||||
class ModBadgeElement : public EmoteElement
|
||||
{
|
||||
public:
|
||||
ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
|
||||
|
||||
protected:
|
||||
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
|
||||
const QSize &size) override;
|
||||
};
|
||||
|
||||
class BadgeElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
|
@ -244,10 +249,24 @@ public:
|
|||
|
||||
EmotePtr getEmote() const;
|
||||
|
||||
protected:
|
||||
virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
|
||||
const QSize &size);
|
||||
|
||||
private:
|
||||
EmotePtr emote_;
|
||||
};
|
||||
|
||||
class ModBadgeElement : public BadgeElement
|
||||
{
|
||||
public:
|
||||
ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
|
||||
|
||||
protected:
|
||||
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
|
||||
const QSize &size) override;
|
||||
};
|
||||
|
||||
// contains a text, formated depending on the preferences
|
||||
class TimestampElement : public MessageElement
|
||||
{
|
||||
|
@ -277,4 +296,56 @@ public:
|
|||
MessageElementFlags flags) override;
|
||||
};
|
||||
|
||||
// contains a full message string that's split into words on space and parses irc colors that are then put into segments
|
||||
// these segments are later passed to "MultiColorTextLayoutElement" elements to be rendered :)
|
||||
class IrcTextElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
IrcTextElement(const QString &text, MessageElementFlags flags,
|
||||
FontStyle style = FontStyle::ChatMedium);
|
||||
~IrcTextElement() override = default;
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
private:
|
||||
FontStyle style_;
|
||||
|
||||
struct Segment {
|
||||
QString text;
|
||||
int fg = -1;
|
||||
int bg = -1;
|
||||
};
|
||||
|
||||
struct Word {
|
||||
QString text;
|
||||
int width = -1;
|
||||
std::vector<Segment> segments;
|
||||
};
|
||||
|
||||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
// Forces a linebreak
|
||||
class LinebreakElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
LinebreakElement(MessageElementFlags flags);
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
};
|
||||
|
||||
// Image element which will pick the quality of the image based on ui scale
|
||||
class ScalingImageElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
ScalingImageElement(ImageSet images, MessageElementFlags flags);
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
private:
|
||||
ImageSet images_;
|
||||
};
|
||||
} // namespace chatterino
|
||||
|
|
399
src/messages/SharedMessageBuilder.cpp
Normal file
399
src/messages/SharedMessageBuilder.cpp
Normal file
|
@ -0,0 +1,399 @@
|
|||
#include "messages/SharedMessageBuilder.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
QUrl getFallbackHighlightSound()
|
||||
{
|
||||
QString path = getSettings()->pathHighlightSound;
|
||||
bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile();
|
||||
|
||||
// Use fallback sound when checkbox is not checked
|
||||
// or custom file doesn't exist
|
||||
if (getSettings()->customHighlightSound && fileExists)
|
||||
{
|
||||
return QUrl::fromLocalFile(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return QUrl("qrc:/sounds/ping2.wav");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SharedMessageBuilder::SharedMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args)
|
||||
: channel(_channel)
|
||||
, ircMessage(_ircMessage)
|
||||
, args(_args)
|
||||
, tags(this->ircMessage->tags())
|
||||
, originalMessage_(_ircMessage->content())
|
||||
, action_(_ircMessage->isAction())
|
||||
{
|
||||
}
|
||||
|
||||
SharedMessageBuilder::SharedMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args, QString content, bool isAction)
|
||||
: channel(_channel)
|
||||
, ircMessage(_ircMessage)
|
||||
, args(_args)
|
||||
, tags(this->ircMessage->tags())
|
||||
, originalMessage_(content)
|
||||
, action_(isAction)
|
||||
{
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
QColor getRandomColor(const QString &v)
|
||||
{
|
||||
int colorSeed = 0;
|
||||
for (const auto &c : v)
|
||||
{
|
||||
colorSeed += c.digitValue();
|
||||
}
|
||||
const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size();
|
||||
return TWITCH_USERNAME_COLORS[colorIndex];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SharedMessageBuilder::parse()
|
||||
{
|
||||
this->parseUsernameColor();
|
||||
|
||||
this->parseUsername();
|
||||
|
||||
this->message().flags.set(MessageFlag::Collapsed);
|
||||
}
|
||||
|
||||
bool SharedMessageBuilder::isIgnored() const
|
||||
{
|
||||
// TODO(pajlada): Do we need to check if the phrase is valid first?
|
||||
auto phrases = getCSettings().ignoredMessages.readOnly();
|
||||
for (const auto &phrase : *phrases)
|
||||
{
|
||||
if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
|
||||
{
|
||||
qDebug() << "Blocking message because it contains ignored phrase"
|
||||
<< phrase.getPattern();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseUsernameColor()
|
||||
{
|
||||
if (getSettings()->colorizeNicknames)
|
||||
{
|
||||
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseUsername()
|
||||
{
|
||||
// username
|
||||
this->userName = this->ircMessage->nick();
|
||||
|
||||
this->message().loginName = this->userName;
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseHighlights()
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
if (this->message().flags.has(MessageFlag::Subscription) &&
|
||||
getSettings()->enableSubHighlight)
|
||||
{
|
||||
if (getSettings()->enableSubHighlightTaskbar)
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (getSettings()->enableSubHighlightSound)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback
|
||||
if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty())
|
||||
{
|
||||
this->highlightSoundUrl_ =
|
||||
QUrl(getSettings()->subHighlightSoundUrl.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor =
|
||||
ColorProvider::instance().color(ColorType::Subscription);
|
||||
|
||||
// This message was a subscription.
|
||||
// Don't check for any other highlight phrases.
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX: Non-common term in SharedMessageBuilder
|
||||
auto currentUser = app->accounts->twitch.getCurrent();
|
||||
|
||||
QString currentUsername = currentUser->getUserName();
|
||||
|
||||
if (getCSettings().isBlacklistedUser(this->ircMessage->nick()))
|
||||
{
|
||||
// Do nothing. We ignore highlights from this user.
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight because it's a whisper
|
||||
if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
|
||||
{
|
||||
if (getSettings()->enableWhisperHighlightTaskbar)
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (getSettings()->enableWhisperHighlightSound)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback
|
||||
if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty())
|
||||
{
|
||||
this->highlightSoundUrl_ =
|
||||
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
this->message().highlightColor =
|
||||
ColorProvider::instance().color(ColorType::Whisper);
|
||||
|
||||
/*
|
||||
* Do _NOT_ return yet, we might want to apply phrase/user name
|
||||
* highlights (which override whisper color/sound).
|
||||
*/
|
||||
}
|
||||
|
||||
// Highlight because of sender
|
||||
auto userHighlights = getCSettings().highlightedUsers.readOnly();
|
||||
for (const HighlightPhrase &userHighlight : *userHighlights)
|
||||
{
|
||||
if (!userHighlight.isMatch(this->ircMessage->nick()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
qDebug() << "Highlight because user" << this->ircMessage->nick()
|
||||
<< "sent a message";
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = userHighlight.getColor();
|
||||
|
||||
if (userHighlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (userHighlight.hasSound())
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
// Use custom sound if set, otherwise use the fallback sound
|
||||
if (userHighlight.hasCustomSound())
|
||||
{
|
||||
this->highlightSoundUrl_ = userHighlight.getSoundUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* User name highlights "beat" highlight phrases: If a message has
|
||||
* all attributes (color, taskbar flashing, sound) set, highlight
|
||||
* phrases will not be checked.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->ircMessage->nick() == currentUsername)
|
||||
{
|
||||
// Do nothing. Highlights cannot be triggered by yourself
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This vector should only be rebuilt upon highlights being changed
|
||||
// fourtf: should be implemented in the HighlightsController
|
||||
std::vector<HighlightPhrase> activeHighlights =
|
||||
getSettings()->highlightedMessages.cloneVector();
|
||||
|
||||
if (getSettings()->enableSelfHighlight && currentUsername.size() > 0)
|
||||
{
|
||||
HighlightPhrase selfHighlight(
|
||||
currentUsername, getSettings()->enableSelfHighlightTaskbar,
|
||||
getSettings()->enableSelfHighlightSound, false, false,
|
||||
getSettings()->selfHighlightSoundUrl.getValue(),
|
||||
ColorProvider::instance().color(ColorType::SelfHighlight));
|
||||
activeHighlights.emplace_back(std::move(selfHighlight));
|
||||
}
|
||||
|
||||
// Highlight because of message
|
||||
for (const HighlightPhrase &highlight : activeHighlights)
|
||||
{
|
||||
if (!highlight.isMatch(this->originalMessage_))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = highlight.getColor();
|
||||
|
||||
if (highlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
// Only set highlightSound_ if it hasn't been set by username
|
||||
// highlights already.
|
||||
if (highlight.hasSound() && !this->highlightSound_)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback sound
|
||||
if (highlight.hasCustomSound())
|
||||
{
|
||||
this->highlightSoundUrl_ = highlight.getSoundUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* Break once no further attributes (taskbar, sound) can be
|
||||
* applied.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote)
|
||||
{
|
||||
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::addTextOrEmoji(const QString &string_)
|
||||
{
|
||||
auto string = QString(string_);
|
||||
|
||||
// Actually just text
|
||||
auto linkString = this->matchLink(string);
|
||||
auto link = Link();
|
||||
auto textColor = this->action_ ? MessageColor(this->usernameColor_)
|
||||
: MessageColor(MessageColor::Text);
|
||||
|
||||
if (linkString.isEmpty())
|
||||
{
|
||||
if (string.startsWith('@'))
|
||||
{
|
||||
this->emplace<TextElement>(string, MessageElementFlag::BoldUsername,
|
||||
textColor, FontStyle::ChatMediumBold);
|
||||
this->emplace<TextElement>(
|
||||
string, MessageElementFlag::NonBoldUsername, textColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->emplace<TextElement>(string, MessageElementFlag::Text,
|
||||
textColor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this->addLink(string, linkString);
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::appendChannelName()
|
||||
{
|
||||
QString channelName("#" + this->channel->getName());
|
||||
Link link(Link::Url, this->channel->getName() + "\n" + this->message().id);
|
||||
|
||||
this->emplace<TextElement>(channelName, MessageElementFlag::ChannelName,
|
||||
MessageColor::System) //
|
||||
->setLink(link);
|
||||
}
|
||||
|
||||
inline QMediaPlayer *getPlayer()
|
||||
{
|
||||
if (isGuiThread())
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
return player;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::triggerHighlights()
|
||||
{
|
||||
static QUrl currentPlayerUrl;
|
||||
|
||||
if (getCSettings().isMutedChannel(this->channel->getName()))
|
||||
{
|
||||
// Do nothing. Pings are muted in this channel.
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||
|
||||
if (this->highlightSound_ && resolveFocus)
|
||||
{
|
||||
if (auto player = getPlayer())
|
||||
{
|
||||
// update the media player url if necessary
|
||||
if (currentPlayerUrl != this->highlightSoundUrl_)
|
||||
{
|
||||
player->setMedia(this->highlightSoundUrl_);
|
||||
|
||||
currentPlayerUrl = this->highlightSoundUrl_;
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
69
src/messages/SharedMessageBuilder.hpp
Normal file
69
src/messages/SharedMessageBuilder.hpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "messages/MessageBuilder.hpp"
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
#include <QColor>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class SharedMessageBuilder : public MessageBuilder
|
||||
{
|
||||
public:
|
||||
SharedMessageBuilder() = delete;
|
||||
|
||||
explicit SharedMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args);
|
||||
|
||||
explicit SharedMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args,
|
||||
QString content, bool isAction);
|
||||
|
||||
QString userName;
|
||||
|
||||
[[nodiscard]] virtual bool isIgnored() const;
|
||||
|
||||
// triggerHighlights triggers any alerts or sounds parsed by parseHighlights
|
||||
virtual void triggerHighlights();
|
||||
virtual MessagePtr build() = 0;
|
||||
|
||||
protected:
|
||||
virtual void parse();
|
||||
|
||||
virtual void parseUsernameColor();
|
||||
|
||||
virtual void parseUsername();
|
||||
|
||||
virtual Outcome tryAppendEmote(const EmoteName &name)
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
|
||||
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
|
||||
virtual void parseHighlights();
|
||||
|
||||
virtual void addTextOrEmoji(EmotePtr emote);
|
||||
virtual void addTextOrEmoji(const QString &value);
|
||||
|
||||
void appendChannelName();
|
||||
|
||||
Channel *channel;
|
||||
const Communi::IrcMessage *ircMessage;
|
||||
MessageParseArgs args;
|
||||
const QVariantMap tags;
|
||||
QString originalMessage_;
|
||||
|
||||
const bool action_{};
|
||||
|
||||
QColor usernameColor_;
|
||||
|
||||
bool highlightAlert_ = false;
|
||||
bool highlightSound_ = false;
|
||||
|
||||
QUrl highlightSoundUrl_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -169,7 +169,7 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
|||
// Painting
|
||||
void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
|
||||
Selection &selection, bool isLastReadMessage,
|
||||
bool isWindowFocused)
|
||||
bool isWindowFocused, bool isMentions)
|
||||
{
|
||||
auto app = getApp();
|
||||
QPixmap *pixmap = this->buffer_.get();
|
||||
|
@ -220,6 +220,14 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
|
|||
app->themes->messages.disabled);
|
||||
}
|
||||
|
||||
if (!isMentions &&
|
||||
this->message_->flags.has(MessageFlag::RedeemedChannelPointReward))
|
||||
{
|
||||
painter.fillRect(
|
||||
0, y, this->scale_ * 4, pixmap->height(),
|
||||
*ColorProvider::instance().color(ColorType::Subscription));
|
||||
}
|
||||
|
||||
// draw selection
|
||||
if (!selection.isEmpty())
|
||||
{
|
||||
|
@ -263,6 +271,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
|
|||
Selection & /*selection*/)
|
||||
{
|
||||
auto app = getApp();
|
||||
auto settings = getSettings();
|
||||
|
||||
QPainter painter(buffer);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
|
@ -296,6 +305,14 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
|
|||
backgroundColor,
|
||||
*ColorProvider::instance().color(ColorType::Subscription));
|
||||
}
|
||||
else if (this->message_->flags.has(MessageFlag::RedeemedHighlight) &&
|
||||
settings->enableRedeemedHighlight.getValue())
|
||||
{
|
||||
// Blend highlight color with usual background color
|
||||
backgroundColor = blendColors(
|
||||
backgroundColor,
|
||||
*ColorProvider::instance().color(ColorType::RedeemedHighlight));
|
||||
}
|
||||
else if (this->message_->flags.has(MessageFlag::AutoMod))
|
||||
{
|
||||
backgroundColor = QColor("#404040");
|
||||
|
|
|
@ -47,7 +47,7 @@ public:
|
|||
// Painting
|
||||
void paint(QPainter &painter, int width, int y, int messageIndex,
|
||||
Selection &selection, bool isLastReadMessage,
|
||||
bool isWindowFocused);
|
||||
bool isWindowFocused, bool isMentions);
|
||||
void invalidateBuffer();
|
||||
void deleteBuffer();
|
||||
void deleteCache();
|
||||
|
|
|
@ -123,9 +123,20 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
|||
xOffset -= element->getRect().width() + this->spaceWidth_;
|
||||
}
|
||||
|
||||
auto yOffset = 0;
|
||||
|
||||
if (element->getCreator().getFlags().has(
|
||||
MessageElementFlag::ChannelPointReward) &&
|
||||
element->getCreator().getFlags().hasNone(
|
||||
{MessageElementFlag::TwitchEmoteImage}))
|
||||
{
|
||||
yOffset -= (this->margin.top * this->scale_);
|
||||
}
|
||||
|
||||
// set move element
|
||||
element->setPosition(QPoint(this->currentX_ + xOffset,
|
||||
this->currentY_ - element->getRect().height()));
|
||||
element->setPosition(
|
||||
QPoint(this->currentX_ + xOffset,
|
||||
this->currentY_ - element->getRect().height() + yOffset));
|
||||
|
||||
// add element
|
||||
this->elements_.push_back(std::unique_ptr<MessageLayoutElement>(element));
|
||||
|
|
|
@ -395,4 +395,41 @@ int TextIconLayoutElement::getXFromIndex(int index)
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// TEXT
|
||||
//
|
||||
|
||||
MultiColorTextLayoutElement::MultiColorTextLayoutElement(
|
||||
MessageElement &_creator, QString &_text, const QSize &_size,
|
||||
std::vector<PajSegment> segments, FontStyle _style, float _scale)
|
||||
: TextLayoutElement(_creator, _text, _size, QColor{}, _style, _scale)
|
||||
, segments_(segments)
|
||||
{
|
||||
this->setText(_text);
|
||||
}
|
||||
|
||||
void MultiColorTextLayoutElement::paint(QPainter &painter)
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
painter.setPen(this->color_);
|
||||
|
||||
painter.setFont(app->fonts->getFont(this->style_, this->scale_));
|
||||
|
||||
int xOffset = 0;
|
||||
|
||||
auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_);
|
||||
|
||||
for (const auto &segment : this->segments_)
|
||||
{
|
||||
// qDebug() << "Draw segment:" << segment.text;
|
||||
painter.setPen(segment.color);
|
||||
painter.drawText(QRectF(this->getRect().x() + xOffset,
|
||||
this->getRect().y(), 10000, 10000),
|
||||
segment.text,
|
||||
QTextOption(Qt::AlignLeft | Qt::AlignTop));
|
||||
xOffset += metrics.width(segment.text);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -107,7 +107,6 @@ protected:
|
|||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(int index) override;
|
||||
|
||||
private:
|
||||
QColor color_;
|
||||
FontStyle style_;
|
||||
float scale_;
|
||||
|
@ -138,4 +137,25 @@ private:
|
|||
QString line2;
|
||||
};
|
||||
|
||||
struct PajSegment {
|
||||
QString text;
|
||||
QColor color;
|
||||
};
|
||||
|
||||
// TEXT
|
||||
class MultiColorTextLayoutElement : public TextLayoutElement
|
||||
{
|
||||
public:
|
||||
MultiColorTextLayoutElement(MessageElement &creator_, QString &text,
|
||||
const QSize &size,
|
||||
std::vector<PajSegment> segments,
|
||||
FontStyle style_, float scale_);
|
||||
|
||||
protected:
|
||||
void paint(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
std::vector<PajSegment> segments_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/Common.hpp"
|
||||
#include "common/Env.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/Link.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
|
@ -12,11 +13,11 @@ namespace chatterino {
|
|||
|
||||
void LinkResolver::getLinkInfo(
|
||||
const QString url, QObject *caller,
|
||||
std::function<void(QString, Link)> successCallback)
|
||||
std::function<void(QString, Link, ImagePtr)> successCallback)
|
||||
{
|
||||
if (!getSettings()->linkInfoTooltip)
|
||||
{
|
||||
successCallback("No link info loaded", Link(Link::Url, url));
|
||||
successCallback("No link info loaded", Link(Link::Url, url), nullptr);
|
||||
return;
|
||||
}
|
||||
// Uncomment to test crashes
|
||||
|
@ -25,30 +26,35 @@ void LinkResolver::getLinkInfo(
|
|||
QUrl::toPercentEncoding(url, "", "/:"))))
|
||||
.caller(caller)
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback, url](auto result) mutable -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto statusCode = root.value("status").toInt();
|
||||
QString response = QString();
|
||||
QString linkString = url;
|
||||
if (statusCode == 200)
|
||||
{
|
||||
response = root.value("tooltip").toString();
|
||||
if (getSettings()->unshortLinks)
|
||||
.onSuccess(
|
||||
[successCallback, url](NetworkResult result) mutable -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto statusCode = root.value("status").toInt();
|
||||
QString response = QString();
|
||||
QString linkString = url;
|
||||
ImagePtr thumbnail = nullptr;
|
||||
if (statusCode == 200)
|
||||
{
|
||||
linkString = root.value("link").toString();
|
||||
response = root.value("tooltip").toString();
|
||||
thumbnail =
|
||||
Image::fromUrl({root.value("thumbnail").toString()});
|
||||
if (getSettings()->unshortLinks)
|
||||
{
|
||||
linkString = root.value("link").toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response = root.value("message").toString();
|
||||
}
|
||||
successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
|
||||
Link(Link::Url, linkString));
|
||||
else
|
||||
{
|
||||
response = root.value("message").toString();
|
||||
}
|
||||
successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
|
||||
Link(Link::Url, linkString), thumbnail);
|
||||
|
||||
return Success;
|
||||
})
|
||||
return Success;
|
||||
})
|
||||
.onError([successCallback, url](auto /*result*/) {
|
||||
successCallback("No link info found", Link(Link::Url, url));
|
||||
successCallback("No link info found", Link(Link::Url, url),
|
||||
nullptr);
|
||||
})
|
||||
.execute();
|
||||
// });
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/Link.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -10,8 +11,9 @@ namespace chatterino {
|
|||
class LinkResolver
|
||||
{
|
||||
public:
|
||||
static void getLinkInfo(const QString url, QObject *caller,
|
||||
std::function<void(QString, Link)> callback);
|
||||
static void getLinkInfo(
|
||||
const QString url, QObject *caller,
|
||||
std::function<void(QString, Link, ImagePtr)> callback);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "messages/Emote.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/ImageSet.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -64,11 +65,12 @@ namespace {
|
|||
|
||||
return {Success, std::move(emotes)};
|
||||
}
|
||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot)
|
||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot,
|
||||
const QString &userName)
|
||||
{
|
||||
auto emotes = EmoteMap();
|
||||
|
||||
auto innerParse = [&jsonRoot, &emotes](const char *key) {
|
||||
auto innerParse = [&jsonRoot, &emotes, &userName](const char *key) {
|
||||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||
for (auto jsonEmote_ : jsonEmotes)
|
||||
{
|
||||
|
@ -76,6 +78,10 @@ namespace {
|
|||
|
||||
auto id = EmoteId{jsonEmote.value("id").toString()};
|
||||
auto name = EmoteName{jsonEmote.value("code").toString()};
|
||||
auto author = EmoteAuthor{jsonEmote.value("user")
|
||||
.toObject()
|
||||
.value("name")
|
||||
.toString()};
|
||||
// emoteObject.value("imageType").toString();
|
||||
|
||||
auto emote = Emote({
|
||||
|
@ -85,7 +91,10 @@ namespace {
|
|||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||
},
|
||||
Tooltip{name.string + "<br />Channel BetterTTV Emote"},
|
||||
Tooltip{name.string + "<br>Channel BetterTTV Emote" +
|
||||
((author.string.isEmpty())
|
||||
? "<br>By: " + userName.toUtf8()
|
||||
: "<br>By: " + author.string)},
|
||||
Url{emoteLinkFormat.arg(id.string)},
|
||||
});
|
||||
|
||||
|
@ -138,17 +147,51 @@ void BttvEmotes::loadEmotes()
|
|||
.execute();
|
||||
}
|
||||
|
||||
void BttvEmotes::loadChannel(const QString &channelId,
|
||||
std::function<void(EmoteMap &&)> callback)
|
||||
void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
|
||||
const QString &channelId, const QString &userName,
|
||||
std::function<void(EmoteMap &&)> callback,
|
||||
bool manualRefresh)
|
||||
{
|
||||
NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelId)
|
||||
.timeout(3000)
|
||||
.onSuccess([callback = std::move(callback)](auto result) -> Outcome {
|
||||
auto pair = parseChannelEmotes(result.parseJson());
|
||||
.onSuccess([callback = std::move(callback), channel, &userName,
|
||||
manualRefresh](auto result) -> Outcome {
|
||||
auto pair = parseChannelEmotes(result.parseJson(), userName);
|
||||
if (pair.first)
|
||||
callback(std::move(pair.second));
|
||||
if (auto shared = channel.lock(); manualRefresh)
|
||||
shared->addMessage(
|
||||
makeSystemMessage("BetterTTV channel emotes reloaded."));
|
||||
return pair.first;
|
||||
})
|
||||
.onError([channelId, channel, manualRefresh](auto result) {
|
||||
auto shared = channel.lock();
|
||||
if (!shared)
|
||||
return;
|
||||
if (result.status() == 203)
|
||||
{
|
||||
// User does not have any BTTV emotes
|
||||
if (manualRefresh)
|
||||
shared->addMessage(makeSystemMessage(
|
||||
"This channel has no BetterTTV channel emotes."));
|
||||
}
|
||||
else if (result.status() == NetworkResult::timedoutStatus)
|
||||
{
|
||||
// TODO: Auto retry in case of a timeout, with a delay
|
||||
qDebug() << "Fetching BTTV emotes for channel" << channelId
|
||||
<< "failed due to timeout";
|
||||
shared->addMessage(makeSystemMessage(
|
||||
"Failed to fetch BetterTTV channel emotes. (timed out)"));
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Error fetching BTTV emotes for channel"
|
||||
<< channelId << ", error" << result.status();
|
||||
shared->addMessage(
|
||||
makeSystemMessage("Failed to fetch BetterTTV channel "
|
||||
"emotes. (unknown error)"));
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "boost/optional.hpp"
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Atomic.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -24,8 +25,10 @@ public:
|
|||
std::shared_ptr<const EmoteMap> emotes() const;
|
||||
boost::optional<EmotePtr> emote(const EmoteName &name) const;
|
||||
void loadEmotes();
|
||||
static void loadChannel(const QString &channelId,
|
||||
std::function<void(EmoteMap &&)> callback);
|
||||
static void loadChannel(std::weak_ptr<Channel> channel,
|
||||
const QString &channelId, const QString &userName,
|
||||
std::function<void(EmoteMap &&)> callback,
|
||||
bool manualRefresh);
|
||||
|
||||
private:
|
||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#include "providers/colors/ColorProvider.hpp"
|
||||
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -38,12 +37,12 @@ QSet<QColor> ColorProvider::recentColors() const
|
|||
* Currently, only colors used in highlight phrases are considered. This
|
||||
* may change at any point in the future.
|
||||
*/
|
||||
for (auto phrase : getApp()->highlights->phrases)
|
||||
for (auto phrase : getSettings()->highlightedMessages)
|
||||
{
|
||||
retVal.insert(*phrase.getColor());
|
||||
}
|
||||
|
||||
for (auto userHl : getApp()->highlights->highlightedUsers)
|
||||
for (auto userHl : getSettings()->highlightedUsers)
|
||||
{
|
||||
retVal.insert(*userHl.getColor());
|
||||
}
|
||||
|
@ -65,7 +64,6 @@ void ColorProvider::initTypeColorMap()
|
|||
{
|
||||
// Read settings for custom highlight colors and save them in map.
|
||||
// If no custom values can be found, set up default values instead.
|
||||
auto backgrounds = getApp()->themes->messages.backgrounds;
|
||||
|
||||
QString customColor = getSettings()->selfHighlightColor;
|
||||
if (QColor(customColor).isValid())
|
||||
|
@ -77,7 +75,8 @@ void ColorProvider::initTypeColorMap()
|
|||
{
|
||||
this->typeColorMap_.insert(
|
||||
{ColorType::SelfHighlight,
|
||||
std::make_shared<QColor>(backgrounds.highlighted)});
|
||||
std::make_shared<QColor>(
|
||||
HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)});
|
||||
}
|
||||
|
||||
customColor = getSettings()->subHighlightColor;
|
||||
|
@ -90,7 +89,7 @@ void ColorProvider::initTypeColorMap()
|
|||
{
|
||||
this->typeColorMap_.insert(
|
||||
{ColorType::Subscription,
|
||||
std::make_shared<QColor>(backgrounds.subscription)});
|
||||
std::make_shared<QColor>(HighlightPhrase::FALLBACK_SUB_COLOR)});
|
||||
}
|
||||
|
||||
customColor = getSettings()->whisperHighlightColor;
|
||||
|
@ -103,22 +102,41 @@ void ColorProvider::initTypeColorMap()
|
|||
{
|
||||
this->typeColorMap_.insert(
|
||||
{ColorType::Whisper,
|
||||
std::make_shared<QColor>(backgrounds.highlighted)});
|
||||
std::make_shared<QColor>(
|
||||
HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)});
|
||||
}
|
||||
|
||||
customColor = getSettings()->redeemedHighlightColor;
|
||||
if (QColor(customColor).isValid())
|
||||
{
|
||||
this->typeColorMap_.insert({ColorType::RedeemedHighlight,
|
||||
std::make_shared<QColor>(customColor)});
|
||||
}
|
||||
else
|
||||
{
|
||||
this->typeColorMap_.insert(
|
||||
{ColorType::RedeemedHighlight,
|
||||
std::make_shared<QColor>(
|
||||
HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR)});
|
||||
}
|
||||
}
|
||||
|
||||
void ColorProvider::initDefaultColors()
|
||||
{
|
||||
// Init default colors
|
||||
this->defaultColors_.emplace_back(31, 141, 43, 127); // Green-ish
|
||||
this->defaultColors_.emplace_back(28, 126, 141, 127); // Blue-ish
|
||||
this->defaultColors_.emplace_back(136, 141, 49, 127); // Golden-ish
|
||||
this->defaultColors_.emplace_back(143, 48, 24, 127); // Red-ish
|
||||
this->defaultColors_.emplace_back(28, 141, 117, 127); // Cyan-ish
|
||||
this->defaultColors_.emplace_back(75, 127, 107, 100); // Teal
|
||||
this->defaultColors_.emplace_back(105, 127, 63, 100); // Olive
|
||||
this->defaultColors_.emplace_back(63, 83, 127, 100); // Blue
|
||||
this->defaultColors_.emplace_back(72, 127, 63, 100); // Green
|
||||
|
||||
auto backgrounds = getApp()->themes->messages.backgrounds;
|
||||
this->defaultColors_.push_back(backgrounds.highlighted);
|
||||
this->defaultColors_.push_back(backgrounds.subscription);
|
||||
this->defaultColors_.emplace_back(31, 141, 43, 115); // Green
|
||||
this->defaultColors_.emplace_back(28, 126, 141, 90); // Blue
|
||||
this->defaultColors_.emplace_back(136, 141, 49, 90); // Golden
|
||||
this->defaultColors_.emplace_back(143, 48, 24, 127); // Red
|
||||
this->defaultColors_.emplace_back(28, 141, 117, 90); // Cyan
|
||||
|
||||
this->defaultColors_.push_back(HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR);
|
||||
this->defaultColors_.push_back(HighlightPhrase::FALLBACK_SUB_COLOR);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue