diff --git a/.gitignore b/.gitignore index 926e849..159153d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ *.pyc *.egg-info /build +/win-build /dist __pycache__ +CMakeLists.txt.user *.swp *~ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bd61c6a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 2.8.11) + +option(ENABLE_TEST "Enable test program build" Off) + +find_package(Qt4 REQUIRED) +set(CMAKE_CXX_FLAGS "-Wall --std=c++11 ${CMAKE_CXX_FLAGS}") + +add_subdirectory(src) + + +if (ENABLE_TEST) +enable_testing() +add_subdirectory(tests) +endif (ENABLE_TEST) diff --git a/README.md b/README.md index 3710dde..b22e36e 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,29 @@ DanmaQ is still under development, documents might be outdated. ### Dependencies -`danmaQ` depends on `requests` and `PyQt4`, you can either install via `pip` or system package manager, -if you use Windows, please download and install python3.4 and PyQt4 manually or download binary version from +`danmaQ` depends on `Qt4` and `qjson`. + +For ArchLinux, run +``` +yaourt -S danmaQ +``` + +For Ubuntu and Debian, run +``` +sudo apt-get install libqjson0 +``` + +For Fedora, run +``` +sudo dnf install qjson +``` + +For OpenSUSE, run +``` +sudo zypper install libqjson0 +``` + +if you use Windows, download bundled binary version from [releases page](https://github.com/bigeagle/danmaQ/releases/). ### Use TUNA Service @@ -19,13 +40,9 @@ if you use Windows, please download and install python3.4 and PyQt4 manually or First u need to create a channel, go to http://dm.tuna.moe/ and create a channel, (let's use `ooxx` as the channel name and `passw0rd` as the password) -then run `python danmaQ.py` and fill `http://dm.tuna.moe` to server, +then run `danmaQ` and fill `http://dm.tuna.moe` to server, and your channel name (`ooxx`) and channel password (`passw0rd)`. -You can configure font and speed by through "preference" dialog. - -![](https://raw.githubusercontent.com/bigeagle/danmaQ/master/screenshots/window.png) - then open http://dm.tuna.moe/ and click to your channel page, then post. ### Self Hosted Service @@ -34,13 +51,13 @@ Clone https://github.com/tuna/gdanmaku-server and run `webserver.py` to start a ### Installation -- **from source**: run `python setup.py install`. +- **from source**: run `mkdir build && cd build && cmake .. && make && make install`. - **windows binary**: https://github.com/bigeagle/danmaQ/releases/ - **Arch Linux**: [AUR](https://aur.archlinux.org/packages/danmaq-git/) ## TODO -- [ ] Multi-Screen support +- [x] Multi-Screen support - [x] Chatting - [ ] Deb package - [ ] RPM package diff --git a/cx_freeze.py b/cx_freeze.py deleted file mode 100644 index e9d26b4..0000000 --- a/cx_freeze.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding:utf-8 -*- -import os -import re -import sys -import site -from cx_Freeze import setup, Executable - - -site_dir = site.getsitepackages()[1] - -# Dependencies are automatically detected, but it might need -# fine tuning. -buildOptions = dict( - packages=['requests', 'danmaQ'], - excludes=[], - include_files=[ - 'LICENSE', - 'danmaQ\images', - # (os.path.join(site_dir, 'PyQt5', 'libEGL.dll'), 'libEGL.dll'), - ], - include_msvcr=True, -) - -name = 'danmaQ' - -if sys.platform == 'win32': - name = name + '.exe' - -base = None -if sys.platform == "win32": - base = "Win32GUI" - -executables = [ - Executable('danmaQ.py', - base=base, - icon=os.path.join("danmaQ", "images", "statusicon.ico"), - targetName=name) -] - -with open("README.md") as f: - readme = f.read() - -__version__ = re.search( - "__version__\s*=\s*'(.*)'", - open('danmaQ/__init__.py').read(), re.M).group(1) -assert __version__ - -setup( - name="danmaQ", - version=__version__, - description="Display danmaku on any screen", - long_description=readme, - author="Justin Wong", - author_email="justin.w.xd@gmail.com", - url="https://github.com/bigeagle/danmaQ/", - license="GPLv3", - options=dict(build_exe=buildOptions), - executables=executables, -) - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ.py b/danmaQ.py deleted file mode 100755 index 492e9ed..0000000 --- a/danmaQ.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from danmaQ import app - -if __name__ == "__main__": - app.main() - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ/__init__.py b/danmaQ/__init__.py deleted file mode 100644 index 3543a78..0000000 --- a/danmaQ/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.13-dev' diff --git a/danmaQ/app.py b/danmaQ/app.py deleted file mode 100644 index bf6c337..0000000 --- a/danmaQ/app.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -import sys -from PyQt4 import QtGui -from datetime import datetime -from . import __version__ -from .danmaq_ui import Danmaku -from .tray_icon import DanmaQTrayIcon, ICON_ENABLED -from .settings import load_config, save_config, multiscreen_manager -from .config_dialog import ConfigDialog -from .subscriber import SubscribeThread - - -class DanmakuApp(QtGui.QWidget): - def __init__(self, parent=None): - super(DanmakuApp, self).__init__(parent) - self.setWindowTitle("Danmaku") - self.setWindowIcon(QtGui.QIcon(ICON_ENABLED)) - - self.trayIcon = DanmaQTrayIcon(self) - self.trayIcon.show() - self.config_dialog = ConfigDialog(self) - self._options = load_config() - - layout = QtGui.QVBoxLayout() - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Server: ", self)) - self._server = QtGui.QLineEdit( - self._options['http_stream_server'], self) - hbox.addWidget(self._server) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Save As Default Server: ", self)) - self._save_server = QtGui.QCheckBox(self) - hbox.addWidget(self._save_server) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Channel: ", self)) - self._chan = QtGui.QLineEdit("demo", self) - hbox.addWidget(self._chan) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Password: ", self)) - self._passwd = QtGui.QLineEdit("", self) - self._passwd.setEchoMode(QtGui.QLineEdit.Password) - hbox.addWidget(self._passwd) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - self.config_button = QtGui.QPushButton("&Preferences", self) - self.hide_button = QtGui.QPushButton("&Hide", self) - self.main_button = QtGui.QPushButton("&Subscribe", self) - hbox.addWidget(self.config_button) - hbox.addWidget(self.hide_button) - hbox.addWidget(self.main_button) - layout.addLayout(hbox) - self.setLayout(layout) - - self.config_button.released.connect(self.config_dialog.show) - self.hide_button.released.connect(self.hide) - self.main_button.released.connect(self.subscribe_danmaku) - self.config_dialog.preferenceChanged.connect(self.apply_new_preference) - self.trayIcon.toggleAction.triggered.connect(self.subscribe_danmaku) - self.trayIcon.showAction.triggered.connect(self.show) - self.trayIcon.configAction.triggered.connect(self.config_dialog.show) - self.trayIcon.aboutAction.triggered.connect(self.show_about_dialog) - self.trayIcon.exitAction.triggered.connect(self.close) - - self.workThread = None - self.dms = {} - self.alert_msg = None - Danmaku.init_lineheight(self) - - def place_center(self): - # Align Center - screenGeo = QtGui.QDesktopWidget().screenGeometry() - self.move( - screenGeo.width() / 2 - self.width() / 2, - screenGeo.height() / 2 - self.height() / 2, - ) - - def subscribe_danmaku(self): - if QtGui.QDesktopWidget().screenCount() > 1: - #self.config_dialog._to_extend_screen.setChecked(True) - self._options['to_extend_screen'] = True - else: - #self.config_dialog._to_extend_screen.setChecked(False) - self._options['to_extend_screen'] = False - #save_config(self._options) - Danmaku.set_options(self._options) - - if self.workThread is None or self.workThread.isFinished(): - self.workThread = SubscribeThread( - "%s" % self._server.text(), - "%s" % self._chan.text(), - "%s" % self._passwd.text(), - parent=self, - ) - self.workThread.started.connect(self.on_subscription_started) - self.workThread.finished.connect(self.on_subscription_finished) - self.workThread.new_danmaku.connect(self.on_new_danmaku) - self.workThread.new_alert.connect(self.on_new_alert) - self.workThread.start() - self.hide() - else: - self.workThread.terminate() - self.workThread = None - - def on_new_danmaku(self, text, style, position): - dm = Danmaku( - text="%s" % text, - style="%s" % style, - position="%s" % position, - parent=self - ) - - dm.exited.connect(self.delete_danmaku) - self.dms[str(id(dm))] = dm - - def on_new_alert(self, msg): - print(msg) - self.alert_msg = "%s" % msg - self.workThread.terminate() - self.workThread = None - - def delete_danmaku(self, _id): - self.dms.pop(str(_id)) - - def on_subscription_started(self): - if self._save_server.isChecked(): - opts = load_config() - opts['http_stream_server'] = self._server.text() - save_config(opts) - - self.main_button.setText("Unsubscribe") - self.trayIcon.set_icon_running() - self.trayIcon.showMessage( - "DanmaQ", - "Subscribing danmaku from {}".format(self.workThread.server) - ) - - def on_subscription_finished(self): - _dms = [dm for _, dm in self.dms.items()] - for dm in _dms: - dm.hide() - dm.clean_close() - self.trayIcon.set_icon_not_running() - self.main_button.setText("Subscribe") - print(self.alert_msg) - if self.alert_msg is not None: - self.trayIcon.showMessage("DanmaQ", self.alert_msg) - self.show() - self.alert_msg = None - else: - self.trayIcon.showMessage("DanmaQ", "Subscription Finished") - - def apply_new_preference(self): - pref = self.config_dialog.preferences() - Danmaku.set_options(pref) - - def show_about_dialog(self): - self.show() - QtGui.QMessageBox.about( - self, - "About DanmaQ", - """ - DanmaQ -

Version: {version}
- Copyright © {year} Justin Wong
- Tsinghua University TUNA Association -

-

- Source Code Available under GPLv3 -
- - https://github.com/bigeagle/danmaQ - -

- """.format( - version=__version__, - year=datetime.now().strftime("%Y"), - ) - ) - - -def init_multiscreen(): - dw = QtGui.QApplication.desktop() - primary_screen_idx = dw.primaryScreen() - - # print("Primary screen: %d" % (primary_screen_idx, )) - screen_geoms = [dw.screenGeometry(i) for i in range(dw.screenCount())] - # print(screen_geoms) - - multiscreen_manager.set_primary(primary_screen_idx) - multiscreen_manager.populate_geometries(screen_geoms) - - -def main(): - import signal - signal.signal(signal.SIGINT, signal.SIG_DFL) - signal.signal(signal.SIGTERM, signal.SIG_DFL) - app = QtGui.QApplication(sys.argv) - init_multiscreen() - danmakuApp = DanmakuApp() - danmakuApp.show() - danmakuApp.place_center() - sys.exit(app.exec_()) - -if __name__ == "__main__": - main() -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ/config_dialog.py b/danmaQ/config_dialog.py deleted file mode 100644 index e03f7b0..0000000 --- a/danmaQ/config_dialog.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import pyqtSignal -from .tray_icon import ICON_ENABLED -from .settings import load_config, save_config - - -class ConfigDialog(QtGui.QDialog): - preferenceChanged = pyqtSignal(name="preferenceChanged") - - def __init__(self, parent=None): - super(ConfigDialog, self).__init__(parent) - self.setWindowTitle("Danmaku") - self.setWindowIcon(QtGui.QIcon(ICON_ENABLED)) - - self._dft = load_config() - - layout = QtGui.QVBoxLayout() - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Font Family: ")) - self._font_family = QtGui.QFontComboBox(self) - self._font_family.setCurrentFont(QtGui.QFont(self._dft['font_family'])) - hbox.addWidget(self._font_family) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Font Size: ")) - self._font_size = QtGui.QSpinBox(self) - self._font_size.setValue(self._dft['font_size']) - hbox.addWidget(self._font_size) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("To Extend Screen? ")) - self._to_extend_screen = QtGui.QCheckBox(self) - if QtGui.QDesktopWidget().screenCount() > 1: - self._to_extend_screen.setChecked(True) - else: - self._to_extend_screen.setChecked(False) - self._to_extend_screen.setEnabled(False) - hbox.addWidget(self._to_extend_screen) - #for i in range(hbox.count()): - # hbox.itemAt(i).hide() - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - hbox.addWidget(QtGui.QLabel("Speed Scale: ")) - self._speed = QtGui.QSlider(QtCore.Qt.Horizontal, self) - self._speed.setTickInterval(1) - self._speed.setMaximum(21) - self._speed.setMinimum(4) - self._speed.setValue(10) - self._speed_indicator = QtGui.QLabel("1.0", self) - self._speed.sliderMoved.connect(self.update_speed_indicator) - hbox.addWidget(self._speed) - hbox.addWidget(self._speed_indicator) - layout.addLayout(hbox) - - hbox = QtGui.QHBoxLayout() - self._save = QtGui.QPushButton("&Save && Apply", self) - self._apply = QtGui.QPushButton("&Apply", self) - self._cancel = QtGui.QPushButton("&Cancel", self) - self._save.released.connect(self.save_preferences) - self._apply.released.connect(self.emit_new_preferences) - self._cancel.released.connect(self.hide) - hbox.addWidget(self._save) - hbox.addWidget(self._apply) - hbox.addWidget(self._cancel) - layout.addLayout(hbox) - - self.setLayout(layout) - - def update_speed_indicator(self): - val = self._speed.value() - self._speed_indicator.setText("{:.1f}".format(val/10.0)) - - def preferences(self): - return { - 'font_family': self._font_family.currentText(), - 'font_size': self._font_size.value(), - 'speed_scale': self._speed.value() / 10.0, - 'to_extend_screen': self._to_extend_screen.isChecked(), - } - - def save_preferences(self): - opts = load_config() - new_opts = self.preferences() - opts['font_family'] = new_opts['font_family'] - opts['font_size'] = new_opts['font_size'] - opts['speed_scale'] = new_opts['speed_scale'] - opts['to_extend_screen'] = new_opts['to_extend_screen'] - save_config(opts) - self.emit_new_preferences() - - def emit_new_preferences(self): - self.preferenceChanged.emit() - self.hide() - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ/danmaq_ui.py b/danmaQ/danmaq_ui.py deleted file mode 100644 index 4bfaddf..0000000 --- a/danmaQ/danmaq_ui.py +++ /dev/null @@ -1,358 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -import sys -import re -from cgi import escape -from random import randint -from threading import Lock - -from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import pyqtSignal - -from .settings import load_config, multiscreen_manager - -color_styles = { - "white": ('rgb(255, 255, 255)', QtGui.QColor("black"), ), - "black": ('rgb(0, 0, 0)', QtGui.QColor("white"), ), - "blue": ('rgb(20, 95, 198)', QtGui.QColor("white"), ), - "cyan": ('rgb(0, 255, 255)', QtGui.QColor("black"), ), - "red": ('rgb(231, 34, 0)', QtGui.QColor("white"), ), - "yellow": ('rgb(255, 221, 2)', QtGui.QColor("black"), ), - "green": ('rgb(4, 202, 0)', QtGui.QColor("black"), ), - "purple": ('rgb(128, 0, 128)', QtGui.QColor("white"), ), -} - -OPTIONS = load_config() - -if sys.platform == "win32": - import win32api - - -class Danmaku(QtGui.QLabel): - _lock = Lock() - vertical_slots = None - fly_slots = None - - _font_family = OPTIONS['font_family'] - _speed_scale = OPTIONS['speed_scale'] - _font_size = OPTIONS['font_size'] - _to_extend_screen = OPTIONS['to_extend_screen'] - _interval = 30 - _style_tmpl = "font-size: {font_size}pt;" \ - + "font-family: {font_family};" \ - + "font-weight: bold;" \ - + "color: {color}; " - _lineheight = 0 - - exited = pyqtSignal(str, name="exited") - - @classmethod - def init_lineheight(cls, par=None): - Danmaku("test", position='top', lifetime=10, parent=par) - - @classmethod - def set_options(cls, opts): - cls._font_family = opts['font_family'] - cls._font_size = opts['font_size'] - cls._speed_scale = opts['speed_scale'] - cls._to_extend_screen = opts['to_extend_screen'] - - @classmethod - def escape_text(cls, text): - text = escape(text) - text = re.sub(r'([^\\])\\n', r'\1
', text) - text = re.sub(r'\\\\n', r'\\n', text) - text = re.sub(r'\[s\](.+)\[/s\]', r'\1', text) - return text - - def __init__(self, text="text", style='white', position='fly', - lifetime=10*1000, parent=None): - text = self.escape_text(text) - super(Danmaku, self).__init__(text, parent) - - self._text = text - self._style = style - self._position = position - self._lifetime = lifetime - - self.setWindowTitle("Danmaku") - self.setStyleSheet("background:transparent; border:none;") - self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) - - self.setWindowFlags( - QtCore.Qt.ToolTip - | QtCore.Qt.FramelessWindowHint - ) - - if sys.platform == "win32": - # Win32 Dark Magic, disable window drop shadows - win32api.SetClassLong( - self.winId().__int__(), - -26, - 0x0008 & ~0x00020000) - - self.init_text(text, style) - - self._width = self.frameSize().width() - self._height = self.frameSize().height() - self._slot = None - - self.screenIdx = multiscreen_manager.get_screen_idx(self._to_extend_screen) - self.screenGeo = multiscreen_manager.geometry(self.screenIdx) - # self._shift = QtGui.QDesktopWidget().availableGeometry(screen=0).width() - self._offset_x = multiscreen_manager.get_offset_x(self.screenIdx, False) - self._origin_y = multiscreen_manager.get_origin_y(self.screenIdx) - with Danmaku._lock: - if Danmaku.vertical_slots is None: - Danmaku._lineheight = self._height - Danmaku.vertical_slots = [0] * \ - ((self.screenGeo.height() - 20) // self._height) - Danmaku.fly_slots = [0] * \ - ((self.screenGeo.height() - 20) // self._height) - - self.quited = False - self.position_inited = False - self.init_position() - - def init_text(self, text, style): - # self.label = QtGui.QLabel(text, parent=self) - tcolor, bcolor = color_styles.get(style, color_styles['white']) - - effect = QtGui.QGraphicsDropShadowEffect(self) - effect.setBlurRadius(7) - effect.setColor(bcolor) - effect.setOffset(0, 0) - - self.setStyleSheet( - self._style_tmpl.format( - font_size=self._font_size, - font_family=self._font_family, - color=tcolor, - ) - ) - - self.setGraphicsEffect(effect) - self.setContentsMargins(0, 0, 0, 0) - - # layout = QtGui.QVBoxLayout() - # layout.addWidget(self.label, 0, QtCore.Qt.AlignVCenter) - # layout.setContentsMargins(0, 0, 0, 0) - # self.setLayout(layout) - - _msize = self.minimumSizeHint() - # _msize.setHeight(self.label.height()+16) - self.resize(_msize) - - def init_position(self): - self.vslots = None - self.fslots = None - self.show() - nlines = self._text.count('
') + 1 - # print("height: {}, lineheight: {}".format(self._height, self._lineheight)) - if self._position == 'fly': - # NOTE: later offseted - self.x = self.screenGeo.width() - - slot = None - with Danmaku._lock: - if nlines > 1: - for i, v in enumerate(self.fly_slots): - if v == 0: - for j in range(nlines): - try: - self.fly_slots[i+j] = self - except IndexError: - break - - slot = i - break - else: - m = len(self.fly_slots) // 2 - _upper = len(self.fly_slots) // 4 - for _ in range(m+1): - i = randint(0, _upper) - if self.fly_slots[i] == 0: - self.fly_slots[i] = self - slot = i - break - _upper *= 2 - if _upper > len(self.fly_slots) - 1: - _upper = len(self.fly_slots) - 1 - # for i in range(m+1)[::-1]: - # if self.fly_slots[i] == 0: - # self.fly_slots[i] = self - # slot = i - # break - # elif self.fly_slots[-i] == 0: - # self.fly_slots[-i] = self - # slot = len(self.fly_slots) - i - # break - # print(len(self.fly_slots), self.fly_slots) - - if slot is None: - self.hide() - QtCore.QTimer.singleShot(10, self.clean_close) - return - else: - self.y = min( - slot * self._lineheight + 20, - self.screenGeo.height() - self._height - 20 - ) - self.fslots = [i+j for j in range(nlines)] - - self.step = ( - (self.screenGeo.width() + self._width) - / (self._lifetime / self._interval) - * self._speed_scale - ) - - QtCore.QTimer.singleShot(self._interval, self.fly) - - elif self._position == 'bottom': - self.x = (self.screenGeo.width() - self._width) / 2 - got = False - with Danmaku._lock: - i = 0 - for i, v in enumerate(Danmaku.vertical_slots[::-1]): - if v == 0: - for j in range(nlines): - try: - Danmaku.vertical_slots[-(i+j+1)] = 1 - except IndexError: - break - self.vslots = [-(i+j+1) for j in range(nlines)] - got = True - break - - if not got: - self.hide() - QtCore.QTimer.singleShot(10, self.clean_close) - return - else: - self.y = (self.screenGeo.height() - + self._lineheight * self.vslots[-1] - 20) - QtCore.QTimer.singleShot(self._lifetime, self.clean_close) - - elif self._position == 'top': - self.x = (self.screenGeo.width() - self._width) / 2 - got = False - with Danmaku._lock: - i = 0 - for i, v in enumerate(Danmaku.vertical_slots): - if v == 0: - for j in range(nlines): - try: - Danmaku.vertical_slots[i+j] = 1 - except IndexError: - break - self.vslots = [i+j for j in range(nlines)] - got = True - break - # else: - # self.hide() - # QtCore.QTimer.singleShot(1000, self.init_position) - # return - - if not got: - self.hide() - QtCore.QTimer.singleShot(10, self.clean_close) - return - else: - self.y = self._lineheight * self.vslots[0] + 20 - QtCore.QTimer.singleShot(self._lifetime, self.clean_close) - - # shift to the extend screen - #if self._to_extend_screen: - # print(self._shift) - # self.x += self._shift - - # true multiscreen - self.x += self._offset_x - self.y += self._origin_y - # print("initial: (%d, %d)" % (self.x, self.y, )) - - self.move(self.x, self.y) - self.position_inited = True - - def fly(self): - _x = int(self.x) - self.x -= self.step - x_dst = int(self.x) - if (self.fly_slots[self.fslots[0]] == self - and self.x + self._width < int(self.screenGeo.width() * 0.4)): - for i in self.fslots: - if i >= len(self.fly_slots): - break - Danmaku.fly_slots[i] = 0 - - if self.x < -self._width: - self.clean_close() - else: - QtCore.QTimer.singleShot(self._interval, self.fly) - - if _x != x_dst: - self.move(x_dst, self.y) - - def clean_close(self): - if self.quited is False: - self.quited = True - - with Danmaku._lock: - # print(Danmaku.count) - if self.vslots is not None: - for i in self.vslots: - if i >= len(self.vertical_slots): - break - Danmaku.vertical_slots[i] = 0 - - self.exited.emit(str(id(self))) - self.destroy() - - -class DanmakuTestApp(QtGui.QDialog): - def __init__(self, parent=None): - super(DanmakuTestApp, self).__init__(parent) - self.setWindowTitle("Danmaku") - self.lineedit = QtGui.QLineEdit("Text") - self.style = QtGui.QLineEdit("blue") - self.position = QtGui.QLineEdit("top") - self.pushbutton = QtGui.QPushButton("Send") - layout = QtGui.QVBoxLayout() - layout.addWidget(self.lineedit) - layout.addWidget(self.style) - layout.addWidget(self.position) - layout.addWidget(self.pushbutton) - self.setLayout(layout) - self.pushbutton.clicked.connect(self.new_danmaku) - self.dms = {} - - def new_danmaku(self): - text = self.lineedit.text() - style = self.style.text() - position = self.position.text() - dm = Danmaku(text, style=style, position=position, parent=self) - dm.exited.connect(self.delete_danmaku) - self.dms[str(id(dm))] = dm - dm.show() - - def delete_danmaku(self, _id): - dm = self.dms.pop(_id) - dm.close() - - -def dm_test(): - import signal - signal.signal(signal.SIGINT, signal.SIG_DFL) - signal.signal(signal.SIGTERM, signal.SIG_DFL) - app = QtGui.QApplication(sys.argv) - danmakuTestApp = DanmakuTestApp() - danmakuTestApp.show() - sys.exit(app.exec_()) - -if __name__ == "__main__": - dm_test() - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ/images/statusicon.ico b/danmaQ/images/statusicon.ico deleted file mode 100644 index 0891536..0000000 Binary files a/danmaQ/images/statusicon.ico and /dev/null differ diff --git a/danmaQ/images/statusicon.png b/danmaQ/images/statusicon.png deleted file mode 100644 index 6b45e74..0000000 Binary files a/danmaQ/images/statusicon.png and /dev/null differ diff --git a/danmaQ/images/statusicon.svg b/danmaQ/images/statusicon.svg deleted file mode 100644 index 17c0089..0000000 --- a/danmaQ/images/statusicon.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/danmaQ/images/statusicon_disabled.png b/danmaQ/images/statusicon_disabled.png deleted file mode 100644 index f1f76ec..0000000 Binary files a/danmaQ/images/statusicon_disabled.png and /dev/null differ diff --git a/danmaQ/settings.py b/danmaQ/settings.py deleted file mode 100644 index d46cae1..0000000 --- a/danmaQ/settings.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -import os -import sys -import json - -DEFAULT_OPTIONS = { - 'http_stream_server': "http://dm.tuna.moe", - 'font_family': "WenQuanYi Micro Hei", - 'font_size': 28, - 'speed_scale': 1.0, - 'to_extend_screen': False, -} - -_xdg_cfg_dir = os.environ.get( - "XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")) - -_cfg_file = os.path.join(_xdg_cfg_dir, "danmaQ", "config.json") - - -def load_config(): - options = dict(DEFAULT_OPTIONS.items()) - - if os.path.exists(_cfg_file): - try: - with open(_cfg_file, 'rb') as f: - if sys.version_info[0] == 3: - s = bytes(f.read()).decode('utf-8') - opts = json.loads(s) - else: - opts = json.load(f) - options['to_extend_screen'] = opts['to_extend_screen'] - options['font_family'] = opts['font_family'] - options['font_size'] = opts['font_size'] - options['speed_scale'] = opts['speed_scale'] - options['http_stream_server'] = opts['http_stream_server'] - except: - options = dict(DEFAULT_OPTIONS.items()) - save_config(options) - else: - if not os.path.exists(os.path.dirname(_cfg_file)): - os.makedirs(os.path.dirname(_cfg_file)) - save_config(options) - - return options - - -def save_config(options): - with open(_cfg_file, 'wb') as f: - if sys.version_info[0] == 3: - s = json.dumps(options, indent=4) - f.write(bytes(s, 'utf-8')) - else: - json.dump(options, f, indent=4) - - -class MultiscreenManager(object): - def __init__(self): - self._primary_screen = 0 - self._geoms = [] - - @property - def primary_screen(self): - return self._primary_screen - - def geometry(self, idx): - return self._geoms[idx] - - def populate_geometries(self, geoms): - self._geoms = geoms - - def set_primary(self, idx): - self._primary_screen = idx - - def get_screen_idx(self, secondary=False): - if len(self._geoms) < 2: - return 0 - - if len(self._geoms) >= 3: - raise NotImplementedError - - if secondary: - return 1 if self._primary_screen == 0 else 0 - else: - return self._primary_screen - - def get_offset_x(self, idx, reversed=False): - geom = self.geometry(idx) - return geom.x() if not reversed else (geom.x() - geom.width()) - - def get_origin_y(self, idx): - geom = self.geometry(idx) - return geom.y() - - -multiscreen_manager = MultiscreenManager() - - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ/subscriber.py b/danmaQ/subscriber.py deleted file mode 100644 index e5d598c..0000000 --- a/danmaQ/subscriber.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding:utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -import json -import uuid -import requests -import time -from PyQt4 import QtCore -from PyQt4.QtCore import pyqtSignal - - -class SubscribeThread(QtCore.QThread): - new_danmaku = pyqtSignal(str, str, str, name="newDanmaku") - new_alert = pyqtSignal(str, name="newAlert") - _uuid = str(uuid.uuid1()) - - def __init__(self, server, channel, passwd, parent=None): - super(SubscribeThread, self).__init__(parent) - self.server = str(server) - self.channel = str(channel) - self.passwd = str(passwd) - - def run(self): - uri = "/api/v1.1/channels/{cname}/danmaku".format(cname=self.channel) - if uri.startswith("/") and self.server.endswith("/"): - server = self.server[:-1] - else: - server = self.server - - url = server + uri - headers = { - "X-GDANMAKU-SUBSCRIBER-ID": self._uuid, - "X-GDANMAKU-AUTH-KEY": self.passwd, - } - - while 1: - try: - res = requests.get(url, headers=headers) - except requests.exceptions.ConnectionError: - time.sleep(1) - continue - if res.status_code == 200 and res.text: - try: - dm_opts = json.loads(res.text) - except: - continue - else: - for dm in dm_opts: - self.new_danmaku.emit( - dm['text'], dm['style'], dm['position']) - - elif res.status_code == 403: - self.new_alert.emit("Wrong Password!") - elif res.status_code == 404: - self.new_alert.emit("Channel does not exist!") - - def __del__(self): - self.wait() - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/danmaQ/tray_icon.py b/danmaQ/tray_icon.py deleted file mode 100644 index 63eafff..0000000 --- a/danmaQ/tray_icon.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -import os -import sys -from PyQt4 import QtGui - -__dir = os.path.dirname(sys.executable) \ - if getattr(sys, 'frozen', False) else \ - os.path.dirname(os.path.realpath(__file__)) - -ICON_DIR = os.path.join(__dir, "images") - -ICON_ENABLED = os.path.join(ICON_DIR, "statusicon.png") -ICON_DISABLED = os.path.join(ICON_DIR, "statusicon_disabled.png") - - -class DanmaQTrayIcon(QtGui.QSystemTrayIcon): - - def __init__(self, parent=None): - self._icon = QtGui.QIcon(ICON_DISABLED) - super(DanmaQTrayIcon, self).__init__(self._icon, parent) - menu = QtGui.QMenu(parent) - self.toggleAction = menu.addAction("Toggle Subscription") - self.showAction = menu.addAction("Show Main Window") - self.configAction = menu.addAction("Preferences") - self.aboutAction = menu.addAction("About") - self.exitAction = menu.addAction("Exit") - self.setContextMenu(menu) - self.activated.connect(self.on_activate) - self.show() - - def on_activate(self, reason): - if reason == self.Trigger: - if self.parent().isVisible(): - self.parent().hide() - else: - self.parent().show() - - def set_icon_not_running(self): - self.setIcon(QtGui.QIcon(ICON_DISABLED)) - - def set_icon_running(self): - self.setIcon(QtGui.QIcon(ICON_ENABLED)) - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/dm_ui_test.py b/dm_ui_test.py deleted file mode 100644 index 7cb0adf..0000000 --- a/dm_ui_test.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -from danmaQ import danmaq_ui - -if __name__ == "__main__": - danmaq_ui.dm_test() - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/setup.py b/setup.py deleted file mode 100755 index c1ffa6f..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -import os -import re - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -packages = ["danmaQ"] -requires = ["requests"] - -with open("README.md") as f: - readme = f.read() - -__version__ = re.search( - "__version__\s*=\s*'(.*)'", - open('danmaQ/__init__.py').read(), re.M).group(1) -assert __version__ - -setup( - name="danmaQ", - version=__version__, - description="Display danmaku on any screen", - long_description=readme, - author="Justin Wong", - author_email="justin.w.xd@gmail.com", - url="https://github.com/bigeagle/danmaQ/", - packages=packages, - package_data={'': ['LICENCE', ], 'danmaQ': [os.path.join('images', '*.png'), ]}, - package_dir={'danmaQ': 'danmaQ'}, - include_package_data=True, - install_requires=requires, - license="GPLv3", - entry_points={ - 'console_scripts': ['danmaQ = danmaQ.app:main'], - }, -) - -# vim: ts=4 sw=4 sts=4 expandtab diff --git a/src/.ycm_extra_conf.py b/src/.ycm_extra_conf.py new file mode 100644 index 0000000..7b56b5b --- /dev/null +++ b/src/.ycm_extra_conf.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python2 +# -*- coding:utf-8 -*- +from __future__ import print_function, division, unicode_literals + +import os +import ycm_core +from clang_helpers import PrepareClangFlags + +# Set this to the absolute path to the folder (NOT the file!) containing the +# compile_commands.json file to use that instead of 'flags'. See here for +# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html +# Most projects will NOT need to set this to anything; you can just change the +# 'flags' list of compilation flags. Notice that YCM itself uses that approach. +compilation_database_folder = '' + +# These are the compilation flags that will be used in case there's no +# compilation database set. +flags = [ +# THIS IS IMPORTANT! Without a "-std=" flag, clang won't know which +# language to use when compiling headers. So it will guess. Badly. So C++ +# headers will be compiled as C headers. You don't want that so ALWAYS specify +# a "-std=". +# For a C project, you would set this to something like 'c99' instead of +# 'c++11'. +'-std=c++11', +# ...and the same thing goes for the magic -x option which specifies the +# language that the files to be compiled are written in. This is mostly +# relevant for c++ headers. +# For a C project, you would set this to 'c' instead of 'c++'. +'-x', +'c++', +'-DQT_CORE_LIB', +'-DQT_GUI_LIB', +'-DQT_NETWORK_LIB', +'-DQT_QML_LIB', +'-DQT_QUICK_LIB', +'-DQT_SQL_LIB', +'-DQT_WIDGETS_LIB', +'-DQT_XML_LIB', + +'-I', '/usr/lib/qt/mkspecs/linux-clang', +'-I', '/usr/include/qt4', +'-I', '/usr/include/qt4/QtCore', +'-I', '/usr/include/qt4/QtGui', +'-I', '/usr/include/qt4/QtNetwork', +'-I', '/usr/include/qt4/QtWidgets', +'-I', '/usr/include/qjson', +'-I', '/usr/include/X11', +'-I', '/usr/include/X11/extensions', + +'-I', '.', +'-I', 'Tests', +'-I', 'build', +'-I', 'build/Tests' +] + +if compilation_database_folder: + database = ycm_core.CompilationDatabase( compilation_database_folder ) +else: + database = None + + +def DirectoryOfThisScript(): + return os.path.dirname( os.path.abspath( __file__ ) ) + + +def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): + if not working_directory: + return flags + new_flags = [] + make_next_absolute = False + path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] + for flag in flags: + new_flag = flag + + if make_next_absolute: + make_next_absolute = False + if not flag.startswith( '/' ): + new_flag = os.path.join( working_directory, flag ) + + for path_flag in path_flags: + if flag == path_flag: + make_next_absolute = True + break + + if flag.startswith( path_flag ): + path = flag[ len( path_flag ): ] + new_flag = path_flag + os.path.join( working_directory, path ) + break + + if new_flag: + new_flags.append( new_flag ) + return new_flags + + +def FlagsForFile( filename ): + if database: + # Bear in mind that compilation_info.compiler_flags_ does NOT return a + # python list, but a "list-like" StringVec object + compilation_info = database.GetCompilationInfoForFile( filename ) + final_flags = PrepareClangFlags( + MakeRelativePathsInFlagsAbsolute( + compilation_info.compiler_flags_, + compilation_info.compiler_working_dir_ ), + filename ) + else: + relative_to = DirectoryOfThisScript() + final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) + + return { + 'flags': final_flags, + 'do_cache': True + } + +# vim: ts=4 sw=4 sts=4 expandtab diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..921d984 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,49 @@ +set(danmaQ_SOURCES + main.cpp + danmaku_ui.cpp + danmaku_window.cpp + danmaQ_app.cpp + subscriber.cpp + ) + +set(danmaQ_HEADERS + subscriber.h + danmaku_ui.h + danmaku_window.h + danmaQ_app.h + danmaku.h + ) + +set(danmaQ_RESOURCES icons.qrc) + +set(CMAKE_AUTOMOC ON) +set(QT_USE_QTNETWORK TRUE) + +include(${QT_USE_FILE}) +add_definitions(${QT_DEFINITIONS}) + +QT4_ADD_RESOURCES(danmaQ_RCC_SRCS ${danmaQ_RESOURCES}) + +if (WIN32) + set(CMAKE_PREFIX_PATH "C:/Program Files/qjson/lib/cmake/qjson/") + find_library (QJSON_LIBRARIES + "C:/Program Files/qjson/lib" + ${QJSON_LIBRARY_DIRS} + ${LIB_INSTALL_DIR} + ) + include_directories("C:/Program Files/qjson/include") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mwindows") +endif(WIN32) + +find_package(QJSON REQUIRED) + +add_executable(danmaQ ${danmaQ_SOURCES} ${danmaQ_HEADERS_MOC} ${danmaQ_RCC_SRCS}) + +set(DANMAQ_LIBRARIES ${QT_LIBRARIES} ${QT_QTNETWORK_LIBRARY} ${QJSON_LIBRARIES}) + +if(CMAKE_SYSTEM_NAME STREQUAL Linux) + find_library(X_LIB Xext) + set(DANMAQ_LIBRARIES ${DANMAQ_LIBRARIES} ${X_LIB}) +endif() + +target_link_libraries(danmaQ ${DANMAQ_LIBRARIES}) diff --git a/src/danmaQ_app.cpp b/src/danmaQ_app.cpp new file mode 100644 index 0000000..2fb85fc --- /dev/null +++ b/src/danmaQ_app.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "danmaku.h" + +DMApp::DMApp() { + + this->setWindowTitle("Danmaku"); + this->setWindowIcon(QIcon(":icon_active.png")); + this->trayIcon = new DMTrayIcon(this); + + QVBoxLayout* layout = new QVBoxLayout(this); + + QHBoxLayout* hbox = new QHBoxLayout(this); + hbox->addWidget(new QLabel("Server: ", this)); + this->server = new QLineEdit("http://dm.tuna.moe", this); + hbox->addWidget(this->server); + layout->addLayout(hbox); + + hbox = new QHBoxLayout(this); + hbox->addWidget(new QLabel("Channel: ", this)); + this->channel = new QLineEdit("demo", this); + hbox->addWidget(this->channel); + layout->addLayout(hbox); + + hbox = new QHBoxLayout(this); + hbox->addWidget(new QLabel("Password: ", this)); + this->passwd = new QLineEdit("", this); + this->passwd->setEchoMode(QLineEdit::Password); + hbox->addWidget(this->passwd); + layout->addLayout(hbox); + + hbox = new QHBoxLayout(this); + this->hideBtn = new QPushButton("&Hide", this); + this->configBtn = new QPushButton("&config", this); + this->configBtn->setEnabled(false); + this->mainBtn = new QPushButton("&Subscribe", this); + hbox->addWidget(this->hideBtn); + hbox->addWidget(this->configBtn); + hbox->addWidget(this->mainBtn); + layout->addLayout(hbox); + + this->setLayout(layout); + + + this->fontSize = 36; + this->lineHeight = this->fontSize * 1.2; + this->fontFamily = QString( + "WenQuanYi Micro Hei, Source Han Sans CN, WenQuanYi Zen Hei," + "Microsoft YaHei, SimHei, " + "STHeiti, Hiragino Sans GB, " + "sans-serif" + ); + this->speedScale = 1.0; + + this->subscriber = NULL; + this->init_windows(); + + connect(this->mainBtn, SIGNAL(released()), this, SLOT(toggle_subscription())); + connect(this->hideBtn, SIGNAL(released()), this, SLOT(hide())); + connect(this->trayIcon->toggleAction, SIGNAL(triggered()), this, SLOT(toggle_subscription())); + connect(this->trayIcon->refreshScreenAction, SIGNAL(triggered()), this, SLOT(reset_windows())); + connect(this->trayIcon->showAction, SIGNAL(triggered()), this, SLOT(show())); + connect(this->trayIcon->aboutAction, SIGNAL(triggered()), this, SLOT(show_about_dialog())); + connect(this->trayIcon->exitAction, SIGNAL(triggered()), this, SLOT(close())); + + + this->show(); + QDesktopWidget desktop; + auto center = desktop.screenGeometry(desktop.primaryScreen()).center(); + this->move(center.x() - this->width()/2, center.y() - this->height()/2); +} + +void DMApp::toggle_subscription() { + if (this->subscriber == NULL || this->subscriber->isFinished()) + { + this->subscriber = new Subscriber(server->text(), channel->text(), passwd->text(), this); + for(auto w=this->dm_windows.begin(); w != this->dm_windows.end(); ++w) { + connect( + this->subscriber, SIGNAL(new_danmaku(QString, QString, QString)), + *w, SLOT(new_danmaku(QString, QString, QString)) + ); + } + connect( + this->subscriber, SIGNAL(started()), + this, SLOT(on_subscription_started()) + ); + connect( + this->subscriber, SIGNAL(finished()), + this, SLOT(on_subscription_stopped()) + ); + connect( + this->subscriber, SIGNAL(new_alert(QString)), + this, SLOT(on_new_alert(QString)) + ); + this->subscriber->start(); + + } else { + this->subscriber->mark_stop = true; + emit stop_subscription(); + if (this->subscriber->wait(1000) == false) { + this->subscriber->terminate(); + } + this->subscriber = NULL; + this->reset_windows(); + } + +} + +void DMApp::init_windows() { + QDesktopWidget desktop; + this->screenCount = desktop.screenCount(); + for (int i=0; idm_windows.append(w); + + if (!(this->subscriber == NULL || this->subscriber->isFinished())) { + connect( + this->subscriber, SIGNAL(new_danmaku(QString, QString, QString)), + w, SLOT(new_danmaku(QString, QString, QString)) + ); + } + } +} + +void DMApp::reset_windows() { + for(auto w=this->dm_windows.begin(); w != this->dm_windows.end(); ++w) { + delete *w; + } + this->dm_windows.clear(); + this->init_windows(); +} + +void DMApp::on_subscription_started() { + myDebug << "Subscription Started"; + this->hide(); + this->trayIcon->set_icon_running(); + this->mainBtn->setText("&Unsubscribe"); + this->trayIcon->showMessage("Subscription Started", "Let's Go"); +} + +void DMApp::on_subscription_stopped() { + myDebug << "Subscription Stopped"; + this->trayIcon->set_icon_stopped(); + this->mainBtn->setText("&Subscribe"); +} + +void DMApp::on_new_alert(QString msg) { + myDebug << "Alert:" << msg; + this->trayIcon->showMessage("Ooops!", msg, QSystemTrayIcon::Critical); + this->subscriber->mark_stop = true; + emit stop_subscription(); + if (this->subscriber->wait(1000) == false) { + this->subscriber->terminate(); + } +} + +void DMApp::show_about_dialog() { + this->show(); + QMessageBox::about( + this, "About", + "DanmaQ" + "

Copyright © 2015 Justin Wong
" + "Tsinghua University TUNA Association

" + "

Source Code Available under GPLv3
" + "" + "https://github.com/bigeagle/danmaQ" + "

" + ); +} + + +DMTrayIcon::DMTrayIcon(QWidget *parent) + :QSystemTrayIcon(parent) +{ + this->icon_running = QIcon(":icon_active.png"); + this->icon_stopped = QIcon(":icon_inactive.png"); + this->setIcon(this->icon_stopped); + + QMenu* menu = new QMenu(parent); + this->toggleAction = menu->addAction("Toggle Subscription"); + this->refreshScreenAction = menu->addAction("Refresh Screen"); + this->showAction = menu->addAction("Show Main Window"); + this->aboutAction = menu->addAction("About"); + this->exitAction = menu->addAction("Exit"); + + this->setContextMenu(menu); + + connect( + this, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, SLOT(on_activated(QSystemTrayIcon::ActivationReason)) + ); + this->show(); +} + +void DMTrayIcon::on_activated(QSystemTrayIcon::ActivationReason e) { + if(e == this->Trigger){ + QWidget *parent = (QWidget *)this->parent(); + if(parent == NULL) { + return; + } + if(parent->isVisible()) { + parent->hide(); + } else { + parent->show(); + } + } +} + +void DMTrayIcon::set_icon_running() { + this->setIcon(this->icon_running); +} + +void DMTrayIcon::set_icon_stopped() { + this->setIcon(this->icon_stopped); +} diff --git a/src/danmaQ_app.h b/src/danmaQ_app.h new file mode 100644 index 0000000..cf0aeb7 --- /dev/null +++ b/src/danmaQ_app.h @@ -0,0 +1,69 @@ +#ifndef __DANMAQ_APP_H__ +#define __DANMAQ_APP_H__ +#include +#include +#include +#include +#include +#include +#include + +class Subscriber; + +class DMTrayIcon: public QSystemTrayIcon +{ + Q_OBJECT + +public: + DMTrayIcon(QWidget *parent=0); + QAction *toggleAction, *showAction, *aboutAction, *exitAction, + *refreshScreenAction; + + +public slots: + void on_activated(QSystemTrayIcon::ActivationReason e); + void set_icon_running(); + void set_icon_stopped(); + +private: + QIcon icon_running, icon_stopped; + +}; + + +class DMApp: public QWidget +{ + Q_OBJECT + +public: + DMApp(); + + int lineHeight, fontSize, screenCount; + QString fontFamily; + float speedScale; + + QLineEdit *server, *channel, *passwd; + QPushButton *configBtn, *hideBtn, *mainBtn; + + +public slots: + void reset_windows(); + void toggle_subscription(); + void on_subscription_started(); + void on_subscription_stopped(); + void on_new_alert(QString msg); + void show_about_dialog(); + +signals: + void stop_subscription(); + +private: + QVector dm_windows; + Subscriber *subscriber; + DMTrayIcon *trayIcon; + void init_windows(); + + +}; + +#endif diff --git a/src/danmaku.h b/src/danmaku.h new file mode 100644 index 0000000..90d0268 --- /dev/null +++ b/src/danmaku.h @@ -0,0 +1,11 @@ +#ifndef __DANMAKU_H__ +#define __DANMAKU_H__ + +#include "danmaku_ui.h" +#include "danmaku_window.h" +#include "subscriber.h" +#include "danmaQ_app.h" + +#define myDebug (qDebug() << "\x1b[34;1m" <<__PRETTY_FUNCTION__ << ":" << __LINE__ << "\x1b[0m") + +#endif diff --git a/src/danmaku_ui.cpp b/src/danmaku_ui.cpp new file mode 100644 index 0000000..72eea33 --- /dev/null +++ b/src/danmaku_ui.cpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if defined _WIN32 || defined __CYGWIN__ +#include +#endif + +#include "danmaku.h" + + +static std::map> colormap = { + {"white", std::make_pair("rgb(255, 255, 255)", QColor("black"))}, + {"black", std::make_pair("rgb(0, 0, 0)", QColor("white"))}, + {"blue", std::make_pair("rgb(20, 95, 198)", QColor("white"))}, + {"cyan", std::make_pair("rgb(0, 255, 255)", QColor("black"))}, + {"red", std::make_pair("rgb(231, 34, 0)", QColor("white"))}, + {"yellow", std::make_pair("rgb(255, 221, 2)", QColor("black"))}, + {"green", std::make_pair("rgb(4, 202, 0)", QColor("black"))}, + {"purple", std::make_pair("rgb(128, 0, 128)", QColor("white"))} +}; + +// Danmaku::Danmaku(QString text, QWidget *parent): Danmaku(text, "blue", FLY, -1, parent){}; + +Danmaku::Danmaku(QString text, QString color, Position position, int slot, DMWindow *parent, DMApp* app) + :QLabel(escape_text(text), parent) +{ + this->dmwin = parent; + this->app = app; + this->setAttribute(Qt::WA_DeleteOnClose); + + QString tcolor = colormap[color].first; + QColor bcolor = colormap[color].second; + + QString style = style_tmpl + .arg(this->app->fontSize) + .arg(this->app->fontFamily) + .arg(tcolor); + + QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect(this); + + bool enableShadow = false; + +#ifndef Q_WS_X11 + if (this->app->screenCount == 1) { + this->setWindowFlags( + Qt::ToolTip + | Qt::FramelessWindowHint + ); + this->setAttribute(Qt::WA_TranslucentBackground, true); + this->setAttribute(Qt::WA_Disabled, true); + this->setAttribute(Qt::WA_TransparentForMouseEvents, true); +#if defined _WIN32 || defined __CYGWIN__ + // remove CS_DROPSHADOW + SetClassLong(this->winId(), -26, 0x0008 & ~0x00020000); +#endif + enableShadow = true; + } else if (position != FLY) { + enableShadow = true; + } +#else + enableShadow = true; +#endif + + myDebug << "enableShadow:" << enableShadow; + + if(enableShadow) { + effect->setBlurRadius(6); + effect->setColor(bcolor); + effect->setOffset(0, 0); + this->setGraphicsEffect(effect); + } + + this->setStyleSheet(style); + this->setContentsMargins(0, 0, 0, 0); + + QSize _msize = this->minimumSizeHint(); + this->resize(_msize); + + this->position = position; + this->slot = slot; + this->init_position(); +} + + +QString Danmaku::escape_text(QString text) { + return text; +} + +QString Danmaku::style_tmpl = QString( + "font-size: %1px;" + "font-weight: bold;" + "font-family: %2;" + "color: %3;" + ); + +void Danmaku::init_position() { + int sw = this->parentWidget()->width(); + this->_y = this->dmwin->slot_y(this->slot); + + switch(this->position) { + case FLY: + myDebug << "fly"; + this->_x = sw; + this->fly(); + break; + case TOP: + case BOTTOM: + this->_x = (sw / 2) - (this->width() / 2); + this->move(this->_x, this->_y); + QTimer::singleShot(10 * 1000, this, SLOT(clean_close())); + break; + } +} + +void Danmaku::fly() { + QPropertyAnimation *animation = new QPropertyAnimation(this, "geometry", this); + animation->setDuration(10 * 1000); + animation->setStartValue( + QRect(this->_x, this->_y, this->width(), this->height())); + + animation->setEndValue( + QRect(-this->width(), this->_y, this->width(), this->height())); + animation->start(QAbstractAnimation::DeleteWhenStopped); + + connect( + animation, SIGNAL(finished()), + this, SLOT(clean_close()) + ); +} + +void Danmaku::clean_close() { + if(this->position == FLY) { + emit clear_fly_slot(this->slot); + } + + this->close(); + emit exited(this); +} + + + diff --git a/src/danmaku_ui.h b/src/danmaku_ui.h new file mode 100644 index 0000000..a87eddf --- /dev/null +++ b/src/danmaku_ui.h @@ -0,0 +1,44 @@ +#ifndef __DANMAKU_UI_H__ +#define __DANMAKU_UI_H__ + +#include +#include +#include + +enum Position { TOP=1, BOTTOM, FLY }; + +const int VMARGIN = 20; + +class DMWindow; +class DMApp; + +class Danmaku: public QLabel +{ +Q_OBJECT + +public: + Danmaku(QString text, QString color, Position position, int slot, DMWindow *parent, DMApp *app); + // Danmaku(QString text, QWidget *parent=0); + Position position; + int slot; + DMWindow *dmwin; + DMApp *app; + +public slots: + void fly(); + void clean_close(); + +signals: + void exited(Danmaku*); + void clear_fly_slot(int slot); + +private: + static QString style_tmpl; + int _x, _y; + + static QString escape_text(QString text); + void init_position(); + +}; + +#endif diff --git a/src/danmaku_window.cpp b/src/danmaku_window.cpp new file mode 100644 index 0000000..f2e5d85 --- /dev/null +++ b/src/danmaku_window.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include + +#ifdef __linux +#include +#include +#endif + +#include "danmaku.h" + + +DMWindow::DMWindow(int screenNumber, DMApp *parent) +{ + this->setParent(parent); + this->app = parent; + QDesktopWidget desktop; + QRect geo = desktop.screenGeometry(screenNumber); + int sw = geo.width(), sh = geo.height(); + myDebug << sw << ", " << sh; + this->resize(sw, sh); + this->setWindowTitle("Danmaku"); + this->setWindowFlags( + Qt::X11BypassWindowManagerHint + | Qt::WindowStaysOnTopHint + | Qt::ToolTip + | Qt::FramelessWindowHint + ); + this->setAttribute(Qt::WA_TranslucentBackground, true); + this->setAttribute(Qt::WA_DeleteOnClose, true); + this->setAttribute(Qt::WA_Disabled, true); + this->setAttribute(Qt::WA_TransparentForMouseEvents, true); + this->setStyleSheet("background: transparent"); + + this->move(geo.topLeft()); + this->init_slots(); + + this->show(); + +#ifdef __linux + QRegion region; + XShapeCombineRegion( + QX11Info::display(), this->winId(), + ShapeInput, 0, 0, region.handle(), ShapeSet); +#endif + +} + +DMWindow::DMWindow(DMApp *parent): DMWindow(0, parent){}; + +void DMWindow::init_slots() +{ + int height = this->height(); + int nlines = (height - 2*VMARGIN) / (this->app->lineHeight); + myDebug << nlines << this->app->lineHeight; + for(int i=0; ifly_slots.append(false); + this->fixed_slots.append(false); + } +} + +int DMWindow::allocate_slot(Position position) { +// if(position == "fly") +// + int slot = -1; + switch (position) { + case FLY: + for (int i=0; i < 6; i++) { + int try_slot; + if (i < 3) { + try_slot = std::rand() % (this->fly_slots.size() / 2); + } else { + try_slot = std::rand() % (this->fly_slots.size()); + } + if(this->fly_slots.at(try_slot) == false) { + this->fly_slots[try_slot] = true; + slot = try_slot; + break; + } + } + break; + case TOP: + for(int i=0; i < this->fixed_slots.size(); i++) { + if(this->fixed_slots.at(i) == false) { + this->fixed_slots[i] = true; + slot = i; + break; + } + } + break; + case BOTTOM: + for(int i=this->fixed_slots.size()-1; i >= 0; i--) { + if(this->fixed_slots.at(i) == false) { + this->fixed_slots[i] = true; + slot = i; + break; + } + } + break; + } + myDebug << "Slot: " << slot; + return slot; +} + +int DMWindow::slot_y(int slot) +{ + return (this->app->lineHeight * slot + VMARGIN); +} + +void DMWindow::new_danmaku(QString text, QString color, QString position) +{ + Position pos; + if(position.compare("fly") == 0) { + myDebug << "fly"; + pos = FLY; + } else if (position.compare("top") == 0) { + myDebug << "top"; + pos = TOP; + } else if (position.compare("bottom") == 0) { + myDebug << "bottom"; + pos = BOTTOM; + } else { + myDebug << "wrong position: " << position; + return; + } + + auto slot = allocate_slot(pos); + if (slot < 0) { + myDebug << "Screen is Full!"; + return; + } + + Danmaku *l = new Danmaku(text, color, pos, slot, this, this->app); + this->connect(l, SIGNAL(exited(Danmaku*)), + this, SLOT(delete_danmaku(Danmaku*))); + this->connect(l, SIGNAL(clear_fly_slot(int)), + this, SLOT(clear_fly_slot(int))); + l->show(); + // l->move(200, 200); +} + +void DMWindow::clear_fly_slot(int slot) { + myDebug << "Clear Flying Slot: " << slot; + // myDebug << this->fly_slots; + this->fly_slots[slot] = false; +} + +void DMWindow::delete_danmaku(Danmaku* dm) { + if (dm->position == TOP || dm->position == BOTTOM) { + this->fixed_slots[dm->slot] = false; + } + myDebug << "danmaku closed"; +} + +DMWindow::~DMWindow() { + myDebug << "window closed"; +} diff --git a/src/danmaku_window.h b/src/danmaku_window.h new file mode 100644 index 0000000..b739941 --- /dev/null +++ b/src/danmaku_window.h @@ -0,0 +1,33 @@ +#ifndef __DANMAKU_WINDOW_H__ +#define __DANMAKU_WINDOW_H__ +#include +#include +#include "danmaQ_app.h" +#include "danmaku_ui.h" + +class DMWindow: public QWidget +{ + Q_OBJECT + +public: + DMWindow(DMApp *parent); + DMWindow(int screenNumber, DMApp *parent); + ~DMWindow(); + DMApp *app; + + int slot_y(int slot); + + +public slots: + void new_danmaku(QString text, QString color, QString position); + void delete_danmaku(Danmaku*); + void clear_fly_slot(int slot); + +private: + QVector fly_slots, fixed_slots; + void init_slots(); + int allocate_slot(Position); + +}; + +#endif diff --git a/src/icons.qrc b/src/icons.qrc new file mode 100644 index 0000000..683e263 --- /dev/null +++ b/src/icons.qrc @@ -0,0 +1,6 @@ + + + icons/statusicon.png + icons/statusicon_disabled.png + + diff --git a/src/icons/statusicon.ico b/src/icons/statusicon.ico new file mode 100644 index 0000000..dcecae1 Binary files /dev/null and b/src/icons/statusicon.ico differ diff --git a/src/icons/statusicon.png b/src/icons/statusicon.png new file mode 100644 index 0000000..7ee4238 Binary files /dev/null and b/src/icons/statusicon.png differ diff --git a/src/icons/statusicon.svg b/src/icons/statusicon.svg new file mode 100644 index 0000000..1987094 --- /dev/null +++ b/src/icons/statusicon.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/icons/statusicon_disabled.png b/src/icons/statusicon_disabled.png new file mode 100644 index 0000000..85fa91f Binary files /dev/null and b/src/icons/statusicon_disabled.png differ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..318c950 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include + +#include "danmaQ_app.h" + + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + std::srand(std::time(0)); + + QDesktopWidget desktop; + DMApp* dm_app = new DMApp(); + app.connect( + &desktop, SIGNAL(workAreaResized(int)), + dm_app, SLOT(reset_windows()) + ); + return app.exec(); +} + diff --git a/src/subscriber.cpp b/src/subscriber.cpp new file mode 100644 index 0000000..e39c0dd --- /dev/null +++ b/src/subscriber.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "danmaku.h" + + +Subscriber::Subscriber(QString server, QString channel, QString passwd, QObject* parent) + : QThread(parent) +{ + this->server = server; + this->channel = channel; + this->passwd = passwd; + + QUuid uuid = QUuid::createUuid(); + this->_uuid = uuid.toString(); + // myDebug << this->_uuid; + + QString uri = QString("/api/v1.1/channels/%1/danmaku").arg(this->channel); + QUrl baseUrl = QUrl(this->server); + // myDebug << baseUrl.host() << baseUrl.port(); + + qint16 port = baseUrl.port(80); + QHttp::ConnectionMode mode = QHttp::ConnectionModeHttp; + + if(baseUrl.scheme().compare("https") == 0) { + port = baseUrl.port(443); + mode = QHttp::ConnectionModeHttps; + } + + + http = new QHttp(baseUrl.host(), mode, port, this); + header = QHttpRequestHeader("GET", uri); + header.setValue("Host", baseUrl.host()); + header.setValue("X-GDANMAKU-SUBSCRIBER-ID", this->_uuid); + header.setValue("X-GDANMAKU-AUTH-KEY", this->passwd); + + // connect(http, SIGNAL(done(bool)), this, SLOT(parse_response(bool))); + connect(this, SIGNAL(terminated()), this, SLOT(deleteLater())); + connect(this, SIGNAL(finished()), this, SLOT(deleteLater())); +} + +void Subscriber::run() +{ + mark_stop = false; + + QEventLoop loop; + connect(http, SIGNAL(done(bool)), &loop, SLOT(quit())); + connect((DMApp *)this->parent(), SIGNAL(stop_subscription()), + &loop, SLOT(quit())); + + // Set HTTP request timeout + QTimer timeout; + timeout.setSingleShot(true); + // If timeout signaled, let http request abort + connect(&timeout, SIGNAL(timeout()), http, SLOT(abort())); + + while(1) { + timeout.start(10000); + http->request(header); + loop.exec(); + timeout.stop(); + if(mark_stop) { + myDebug << "Thread marked to stop"; + break; + } + if(http->error()){ + myDebug << http->errorString() << "Wait 2 secs"; + this->msleep(2000); + } else { + parse_response(); + } + } +} + + +void Subscriber::parse_response() { + QHttpResponseHeader resp = http->lastResponse(); + if(resp.isValid()) { + bool fatal = false; + int statusCode = resp.statusCode(); + if (statusCode >= 400) { + fatal = true; + QString errMsg; + if (statusCode == 403 ) { + errMsg = "Wrong Password"; + } else if (statusCode == 404) { + errMsg = "No Such Channel"; + } else if (statusCode >= 500) { + errMsg = "Server Error"; + } + myDebug << errMsg; + emit new_alert(errMsg); + } + if (fatal) { + return; + } + } + + bool ok; + QJson::Parser parser; + + // QByteArray json = QByteArray( + // "[{\"text\": \"test\", \"style\": \"white\", \"position\": \"fly\"}," + // "{\"text\": \"test2\", \"style\": \"white\", \"position\": \"fly\"}]" + // ); + QByteArray json = http->readAll(); + + QVariant res = parser.parse(json, &ok); + + if(ok) { + QVariantList dms = res.toList(); + for(QVariantList::iterator i = dms.begin(); i != dms.end(); ++i) { + QVariantMap dm = i->toMap(); + QString text = dm["text"].toString(), + color = dm["style"].toString(), + position = dm["position"].toString(); + myDebug << text ; + + emit new_danmaku(text, color, position); + } + } +} + diff --git a/src/subscriber.h b/src/subscriber.h new file mode 100644 index 0000000..689bb89 --- /dev/null +++ b/src/subscriber.h @@ -0,0 +1,33 @@ +#ifndef __DANMAKU_SUBSCRIBER_H__ +#define __DANMAKU_SUBSCRIBER_H__ +#include +#include +#include +#include + + +class Subscriber : public QThread +{ + Q_OBJECT + +public: + Subscriber(QString server, QString channel, QString passwd, QObject* parent=0); + void run(); + bool mark_stop; + +public slots: + void parse_response(); + +signals: + void new_danmaku(QString text, QString color, QString position); + void new_alert(QString msg); + +private: + QHttp* http; + QHttpRequestHeader header; + QString server, channel, passwd, _uuid; + +}; + + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..6697850 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,3 @@ +include_directories(${PROJECT_SOURCE_DIR}/src) + + diff --git a/tests/json_test.cpp b/tests/json_test.cpp new file mode 100644 index 0000000..f1e9512 --- /dev/null +++ b/tests/json_test.cpp @@ -0,0 +1 @@ +#include "json_message.h"