提交 5ba8b09c 编写于 作者: J jp9000

Add help menu with log file uploading

Added github gist API uploading to the help menu to help make problems a
bit easier to debug in the future.  It's somewhat vital that this
functionality be implemented before any release in order to analyze any
given problem a user may be experiencing.
上级 b64900e6
......@@ -34,6 +34,11 @@ Output.ConnectFail.InvalidStream="Could not access the specified channel or stre
Output.ConnectFail.Error="An unexpected error occurred when trying to connect to the server. More information in the log file."
Output.ConnectFail.Disconnected="Disconnected from server."
# log upload dialog text and messages
LogReturnDialog="Log Upload Successful"
LogReturnDialog.CopyURL="Copy URL"
LogReturnDialog.ErrorUploadingLog="Error uploading log file"
# audio device names
Basic.DesktopDevice1="Desktop Audio"
Basic.DesktopDevice2="Desktop Audio 2"
......@@ -70,13 +75,19 @@ Basic.Main.StartStreaming="Start Streaming"
Basic.Main.StopRecording="Stop Recording"
Basic.Main.StopStreaming="Stop Streaming"
# basic mode menu
# basic mode file menu
Basic.MainMenu.File="&File"
Basic.MainMenu.File.Export="&Export"
Basic.MainMenu.File.Import="&Import"
Basic.MainMenu.File.Settings="&Settings"
Basic.MainMenu.File.Exit="E&xit"
# basic mode help menu
Basic.MainMenu.Help="&Help"
Basic.MainMenu.Help.Logs="&Log Files"
Basic.MainMenu.Help.Logs.UploadCurrentLog="Upload &Current Log File"
Basic.MainMenu.Help.Logs.UploadLastLog="Upload &Last Log File"
# basic mode settings dialog
Basic.Settings.ProgramRestart="The program must be restarted for these settings to take effect."
Basic.Settings.ConfirmTitle="Confirm Changes"
......
......@@ -89,7 +89,7 @@ extern "C" {
*
* Reset to zero each major or minor version
*/
#define LIBOBS_API_PATCH_VER 2
#define LIBOBS_API_PATCH_VER 3
#define LIBOBS_API_VER ((LIBOBS_API_MAJOR_VER << 24) | \
(LIBOBS_API_MINOR_VER << 16) | \
......
......@@ -130,7 +130,8 @@ os_dir_t os_opendir(const char *path)
return NULL;
dir = bzalloc(sizeof(struct os_dir));
dir->dir = dir_val;
dir->dir = dir_val;
dir->path = path;
return dir;
}
......
......@@ -18,6 +18,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR TRUE)
set(CMAKE_AUTOMOC TRUE)
find_package(Qt5Widgets REQUIRED)
find_package(Qt5Network REQUIRED)
if(WIN32)
set(obs_PLATFORM_SOURCES
......@@ -59,6 +60,7 @@ set(obs_SOURCES
window-basic-properties.cpp
window-basic-source-select.cpp
window-namedialog.cpp
window-log-reply.cpp
properties-view.cpp
volume-control.cpp
qt-wrappers.cpp)
......@@ -72,6 +74,7 @@ set(obs_HEADERS
window-basic-properties.hpp
window-basic-source-select.hpp
window-namedialog.hpp
window-log-reply.hpp
properties-view.hpp
display-helpers.hpp
volume-control.hpp
......@@ -80,6 +83,7 @@ set(obs_HEADERS
set(obs_UI
forms/NameDialog.ui
forms/OBSLogReply.ui
forms/OBSBasic.ui
forms/OBSBasicSettings.ui
forms/OBSBasicSourceSelect.ui
......@@ -98,6 +102,7 @@ add_executable(obs WIN32
${obs_QRC_SOURCES})
qt5_use_modules(obs Widgets ${obs_PLATFORM_QT_MODULES})
qt5_use_modules(obs Network ${obs_PLATFORM_QT_MODULES})
if(WIN32)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
......
......@@ -450,7 +450,21 @@
<addaction name="separator"/>
<addaction name="actionE_xit"/>
</widget>
<widget class="QMenu" name="menuBasic_MainMenu_Help">
<property name="title">
<string>Basic.MainMenu.Help</string>
</property>
<widget class="QMenu" name="menuLogFiles">
<property name="title">
<string>Basic.MainMenu.Help.Logs</string>
</property>
<addaction name="actionUploadCurrentLog"/>
<addaction name="actionUploadLastLog"/>
</widget>
<addaction name="menuLogFiles"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menuBasic_MainMenu_Help"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionAddScene">
......@@ -586,6 +600,16 @@
<string>Basic.MainMenu.File.Exit</string>
</property>
</action>
<action name="actionUploadLastLog">
<property name="text">
<string>Basic.MainMenu.Help.Logs.UploadLastLog</string>
</property>
</action>
<action name="actionUploadCurrentLog">
<property name="text">
<string>Basic.MainMenu.Help.Logs.UploadCurrentLog</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OBSLogReply</class>
<widget class="QDialog" name="OBSLogReply">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>95</height>
</rect>
</property>
<property name="windowTitle">
<string>LogReturnDialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="urlEdit">
<property name="text">
<string notr="true"/>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copyURL">
<property name="text">
<string>LogReturnDialog.CopyURL</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OBSLogReply</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OBSLogReply</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
......@@ -44,13 +44,38 @@ using namespace std;
static log_handler_t def_log_handler;
static string currentLogFile;
static string lastLogFile;
string CurrentTimeString()
{
time_t now = time(0);
struct tm tstruct;
char buf[80];
tstruct = *localtime(&now);
strftime(buf, sizeof(buf), "%X", &tstruct);
return buf;
}
string CurrentDateTimeString()
{
time_t now = time(0);
struct tm tstruct;
char buf[80];
tstruct = *localtime(&now);
strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct);
return buf;
}
static void do_log(int log_level, const char *msg, va_list args, void *param)
{
fstream &logFile = *static_cast<fstream*>(param);
char str[4096];
va_list args2;
#ifndef _WIN32
va_copy(args2, args);
#endif
vsnprintf(str, 4095, msg, args);
......@@ -62,7 +87,7 @@ static void do_log(int log_level, const char *msg, va_list args, void *param)
#endif
if (log_level <= LOG_INFO)
logFile << str << endl;
logFile << CurrentTimeString() << ": " << str << endl;
#ifdef _WIN32
if (log_level <= LOG_ERROR && IsDebuggerPresent())
......@@ -190,10 +215,8 @@ const char *OBSApp::GetRenderModule() const
const char *renderer = config_get_string(globalConfig, "Video",
"Renderer");
if (astrcmpi(renderer, "Direct3D 11") == 0)
return "libobs-d3d11";
else
return "libobs-opengl";
return (astrcmpi(renderer, "Direct3D 11") == 0) ?
"libobs-d3d11" : "libobs-opengl";
}
void OBSApp::OBSInit()
......@@ -210,18 +233,24 @@ string OBSApp::GetVersionString() const
LIBOBS_API_MINOR_VER << "." <<
LIBOBS_API_PATCH_VER;
ver << " (";
#ifdef HAVE_OBSCONFIG_H
ver << " (" << OBS_VERSION << ")";
ver << OBS_VERSION << ", ";
#endif
#ifdef _WIN32
if (sizeof(void*) == 8)
ver << " (64bit)";
ver << "64bit, ";
else
ver << " (32bit)";
#endif
ver << "32bit, ";
blog(LOG_INFO, "%s", ver.str().c_str());
ver << "windows)";
#elif __APPLE__
ver << "mac)";
#else /* assume linux for the time being */
ver << "linux)";
#endif
return ver.str();
}
......@@ -247,6 +276,16 @@ const char *OBSApp::OutputAudioSource() const
return OUTPUT_AUDIO_SOURCE;
}
const char *OBSApp::GetLastLog() const
{
return lastLogFile.c_str();
}
const char *OBSApp::GetCurrentLog() const
{
return currentLogFile.c_str();
}
QString OBSTranslator::translate(const char *context, const char *sourceText,
const char *disambiguation, int n) const
{
......@@ -273,10 +312,57 @@ struct NoFocusFrameStyle : QProxyStyle
}
};
static bool get_token(lexer *lex, string &str, base_token_type type)
{
base_token token;
if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE))
return false;
if (token.type != type)
return false;
str.assign(token.text.array, token.text.len);
return true;
}
static bool expect_token(lexer *lex, const char *str, base_token_type type)
{
base_token token;
if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE))
return false;
if (token.type != type)
return false;
return strref_cmp(&token.text, str) == 0;
}
static uint64_t convert_log_name(const char *name)
{
BaseLexer lex;
string year, month, day, hour, minute, second;
lexer_start(lex, name);
if (!get_token(lex, year, BASETOKEN_DIGIT)) return 0;
if (!expect_token(lex, "-", BASETOKEN_OTHER)) return 0;
if (!get_token(lex, month, BASETOKEN_DIGIT)) return 0;
if (!expect_token(lex, "-", BASETOKEN_OTHER)) return 0;
if (!get_token(lex, day, BASETOKEN_DIGIT)) return 0;
if (!get_token(lex, hour, BASETOKEN_DIGIT)) return 0;
if (!expect_token(lex, "-", BASETOKEN_OTHER)) return 0;
if (!get_token(lex, minute, BASETOKEN_DIGIT)) return 0;
if (!expect_token(lex, "-", BASETOKEN_OTHER)) return 0;
if (!get_token(lex, second, BASETOKEN_DIGIT)) return 0;
stringstream timestring;
timestring << year << month << day << hour << minute << second;
return std::stoull(timestring.str());
}
static void delete_oldest_log(void)
{
BPtr<char> logDir(os_get_config_path("obs-studio/logs"));
char firstLog[256] = {};
string oldestLog;
uint64_t oldest_ts = -1;
struct os_dirent *entry;
unsigned int maxLogs = (unsigned int)config_get_uint(
......@@ -287,36 +373,64 @@ static void delete_oldest_log(void)
unsigned int count = 0;
while ((entry = os_readdir(dir)) != NULL) {
if (entry->directory)
if (entry->directory || *entry->d_name == '.')
continue;
/* no hidden files */
if (*entry->d_name == '.')
continue;
uint64_t ts = convert_log_name(entry->d_name);
if (!*firstLog)
strncpy(firstLog, entry->d_name, 255);
if (ts) {
if (ts < oldest_ts) {
oldestLog = entry->d_name;
oldest_ts = ts;
}
count++;
count++;
}
}
os_closedir(dir);
if (count > maxLogs && *firstLog) {
if (count > maxLogs) {
stringstream delPath;
delPath << logDir << "/" << firstLog;
delPath << logDir << "/" << oldestLog;
os_unlink(delPath.str().c_str());
}
}
}
static void get_last_log(void)
{
BPtr<char> logDir(os_get_config_path("obs-studio/logs"));
struct os_dirent *entry;
os_dir_t dir = os_opendir(logDir);
uint64_t highest_ts = 0;
if (dir) {
while ((entry = os_readdir(dir)) != NULL) {
if (entry->directory || *entry->d_name == '.')
continue;
uint64_t ts = convert_log_name(entry->d_name);
if (ts > highest_ts) {
lastLogFile = entry->d_name;
highest_ts = ts;
}
}
os_closedir(dir);
}
}
static void create_log_file(fstream &logFile)
{
stringstream dst;
time_t now = time(0);
struct tm *cur_time;
get_last_log();
cur_time = localtime(&now);
if (cur_time) {
char file[256] = {};
......@@ -329,6 +443,8 @@ static void create_log_file(fstream &logFile)
cur_time->tm_min,
cur_time->tm_sec);
currentLogFile = file;
dst << "obs-studio/logs/" << file;
BPtr<char> path(os_get_config_path(dst.str().c_str()));
......
......@@ -19,12 +19,24 @@
#include <QApplication>
#include <QTranslator>
#include <util/lexer.h>
#include <util/util.hpp>
#include <string>
#include <memory>
#include "window-main.hpp"
std::string CurrentTimeString();
std::string CurrentDateTimeString();
struct BaseLexer {
lexer lex;
public:
inline BaseLexer() {lexer_init(&lex);}
inline ~BaseLexer() {lexer_free(&lex);}
operator lexer*() {return &lex;}
};
class OBSTranslator : public QTranslator {
Q_OBJECT
......@@ -69,6 +81,9 @@ public:
return textLookup.GetString(lookupVal);
}
const char *GetLastLog() const;
const char *GetCurrentLog() const;
std::string GetVersionString() const;
const char *InputAudioSource() const;
......
......@@ -20,7 +20,10 @@
#include <QMessageBox>
#include <QShowEvent>
#include <QFileDialog>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <util/dstr.h>
#include <util/util.hpp>
#include <util/platform.h>
......@@ -31,12 +34,14 @@
#include "window-basic-source-select.hpp"
#include "window-basic-main.hpp"
#include "window-basic-properties.hpp"
#include "window-log-reply.hpp"
#include "qt-wrappers.hpp"
#include "display-helpers.hpp"
#include "volume-control.hpp"
#include "ui_OBSBasic.h"
#include <fstream>
#include <sstream>
#include <QScreen>
......@@ -50,14 +55,14 @@ Q_DECLARE_METATYPE(order_movement);
OBSBasic::OBSBasic(QWidget *parent)
: OBSMainWindow (parent),
properties (nullptr),
streamOutput (nullptr),
service (nullptr),
aac (nullptr),
x264 (nullptr),
sceneChanging (false),
resizeTimer (0),
ui (new Ui::OBSBasic)
properties (nullptr),
streamOutput (nullptr),
service (nullptr),
aac (nullptr),
x264 (nullptr),
sceneChanging (false),
resizeTimer (0),
ui (new Ui::OBSBasic)
{
ui->setupUi(this);
......@@ -71,6 +76,7 @@ OBSBasic::OBSBasic(QWidget *parent)
stringstream name;
name << "OBS " << App()->GetVersionString();
blog(LOG_INFO, "%s", name.str().c_str());
setWindowTitle(QT_UTF8(name.str().c_str()));
}
......@@ -1216,6 +1222,106 @@ void OBSBasic::on_actionSourceDown_triggered()
obs_sceneitem_setorder(item, ORDER_MOVE_DOWN);
}
static char *ReadLogFile(const char *log)
{
BPtr<char> logDir(os_get_config_path("obs-studio/logs"));
string path = (char*)logDir;
path += "/";
path += log;
char *file = os_quick_read_utf8_file(path.c_str());
if (!file)
blog(LOG_WARNING, "Failed to read log file %s", path.c_str());
return file;
}
void OBSBasic::UploadLog(const char *file)
{
dstr fileString = {};
stringstream ss;
string jsonData;
dstr_move_array(&fileString, ReadLogFile(file));
if (!fileString.array)
return;
if (!*fileString.array) {
dstr_free(&fileString);
return;
}
ui->menuLogFiles->setEnabled(false);
dstr_replace(&fileString, "\\", "\\\\");
dstr_replace(&fileString, "\"", "\\\"");
dstr_replace(&fileString, "\n", "\\n");
dstr_replace(&fileString, "\r", "\\r");
dstr_replace(&fileString, "\t", "\\t");
dstr_replace(&fileString, "\t", "\\t");
dstr_replace(&fileString, "/", "\\/");
ss << "{ \"public\": false, \"description\": \"OBS " <<
App()->GetVersionString() << " log file uploaded at " <<
CurrentDateTimeString() << "\", \"files\": { \"" <<
file << "\": { \"content\": \"" <<
fileString.array << "\" } } }";
jsonData = std::move(ss.str());
logUploadPostData.setData(jsonData.c_str(), jsonData.size());
QUrl url("https://api.github.com/gists");
logUploadReply = networkManager.post(QNetworkRequest(url),
&logUploadPostData);
connect(logUploadReply, SIGNAL(finished()),
this, SLOT(logUploadFinished()));
connect(logUploadReply, SIGNAL(readyRead()),
this, SLOT(logUploadRead()));
dstr_free(&fileString);
}
void OBSBasic::on_actionUploadCurrentLog_triggered()
{
UploadLog(App()->GetCurrentLog());
}
void OBSBasic::on_actionUploadLastLog_triggered()
{
UploadLog(App()->GetLastLog());
}
void OBSBasic::logUploadRead()
{
logUploadReturnData.push_back(logUploadReply->readAll());
}
void OBSBasic::logUploadFinished()
{
ui->menuLogFiles->setEnabled(true);
if (logUploadReply->error()) {
QMessageBox::information(this,
QTStr("LogReturnDialog.ErrorUploadingLog"),
logUploadReply->errorString());
return;
}
const char *jsonReply = logUploadReturnData.constData();
if (!jsonReply || !*jsonReply)
return;
obs_data_t returnData = obs_data_create_from_json(jsonReply);
QString logURL = obs_data_getstring(returnData, "html_url");
obs_data_release(returnData);
OBSLogReply logDialog(this, logURL);
logDialog.exec();
}
void OBSBasic::StreamingStart()
{
ui->streamButton->setText("Stop Streaming");
......
......@@ -17,6 +17,8 @@
#pragma once
#include <QNetworkAccessManager>
#include <QBuffer>
#include <obs.hpp>
#include <unordered_map>
#include <vector>
......@@ -30,6 +32,7 @@
class QListWidgetItem;
class VolControl;
class QNetworkReply;
#include "ui_OBSBasic.h"
......@@ -49,6 +52,12 @@ private:
QPointer<OBSBasicProperties> properties;
QNetworkAccessManager networkManager;
QBuffer logUploadPostData;
QNetworkReply *logUploadReply;
QByteArray logUploadReturnData;
obs_output_t streamOutput;
obs_service_t service;
obs_encoder_t aac;
......@@ -66,6 +75,8 @@ private:
void ClearVolumeControls();
void UploadLog(const char *file);
void Save(const char *file);
void Load(const char *file);
......@@ -176,9 +187,14 @@ private slots:
void on_actionSourceProperties_triggered();
void on_actionSourceUp_triggered();
void on_actionSourceDown_triggered();
void on_actionUploadCurrentLog_triggered();
void on_actionUploadLastLog_triggered();
void on_streamButton_clicked();
void on_settingsButton_clicked();
void logUploadRead();
void logUploadFinished();
public:
explicit OBSBasic(QWidget *parent = 0);
virtual ~OBSBasic();
......
......@@ -34,14 +34,6 @@
using namespace std;
struct BaseLexer {
lexer lex;
public:
inline BaseLexer() {lexer_init(&lex);}
inline ~BaseLexer() {lexer_free(&lex);}
operator lexer*() {return &lex;}
};
/* parses "[width]x[height]", string, i.e. 1024x768 */
static bool ConvertResText(const char *res, uint32_t &cx, uint32_t &cy)
{
......
/******************************************************************************
Copyright (C) 2014 by Hugh Bailey <obs.jim@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include <QClipboard>
#include "window-log-reply.hpp"
OBSLogReply::OBSLogReply(QWidget *parent, const QString &url)
: QDialog (parent),
ui (new Ui::OBSLogReply)
{
ui->setupUi(this);
ui->urlEdit->setText(url);
}
void OBSLogReply::on_copyURL_clicked()
{
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(ui->urlEdit->text());
}
/******************************************************************************
Copyright (C) 2014 by Hugh Bailey <obs.jim@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <memory>
#include "ui_OBSLogReply.h"
class OBSLogReply : public QDialog {
Q_OBJECT
private:
std::unique_ptr<Ui::OBSLogReply> ui;
public:
OBSLogReply(QWidget *parent, const QString &url);
private slots:
void on_copyURL_clicked();
};
......@@ -39,6 +39,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "obs-ffmpeg", "obs-ffmpeg\ob
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "obs-studio", "obs-studio\obs-studio.vcxproj", "{B12702AD-ABFB-343A-A199-8E24837244A3}"
ProjectSection(ProjectDependencies) = postProject
{C7549517-7FAB-46A5-BB21-228984573265} = {C7549517-7FAB-46A5-BB21-228984573265}
{6F1AC2AE-6424-401A-AF9F-A771E6BEE026} = {6F1AC2AE-6424-401A-AF9F-A771E6BEE026}
EndProjectSection
EndProject
......
......@@ -83,6 +83,12 @@
<CustomBuild Include="..\..\..\obs\window-basic-source-select.hpp">
<Filter>Header Files</Filter>
</CustomBuild>
<CustomBuild Include="..\..\..\obs\window-log-reply.hpp">
<Filter>Header Files</Filter>
</CustomBuild>
<CustomBuild Include="..\..\..\obs\forms\OBSLogReply.ui">
<Filter>Form Files</Filter>
</CustomBuild>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\..\obs\platform.hpp">
......@@ -112,6 +118,9 @@
<ClInclude Include="GeneratedFiles\ui_OBSBasicSourceSelect.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="GeneratedFiles\ui_OBSLogReply.h">
<Filter>Generated Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\..\obs\obs-app.cpp">
......@@ -207,6 +216,15 @@
<ClCompile Include="GeneratedFiles\Release\moc_window-basic-source-select.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="GeneratedFiles\Debug\moc_window-log-reply.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="GeneratedFiles\Release\moc_window-log-reply.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="..\..\..\obs\window-log-reply.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Image Include="..\..\..\obs\forms\images\add.ico">
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册