要提交的变更: 修改: README.md 新文件: README_zh-CN.md 新文件: assets/CHANGELOG.md 新文件: assets/Parsedown.php 新文件: assets/css/login.css 新文件: assets/css/manage.css 新文件: assets/defaultConfig.json 新文件: assets/defaultIconList.json 新文件: assets/html/favicon.ico 新文件: assets/html/login.html 新文件: assets/html/manage.html 新文件: assets/js/console.js 新文件: assets/js/manage.js 新文件: assets/opencc/composer.json 新文件: assets/opencc/composer.lock 新文件: assets/opencc/vendor/autoload.php 新文件: assets/opencc/vendor/bin/opencc 新文件: assets/opencc/vendor/composer/ClassLoader.php 新文件: assets/opencc/vendor/composer/InstalledVersions.php 新文件: assets/opencc/vendor/composer/LICENSE 新文件: assets/opencc/vendor/composer/autoload_classmap.php 新文件: assets/opencc/vendor/composer/autoload_files.php 新文件: assets/opencc/vendor/composer/autoload_namespaces.php 新文件: assets/opencc/vendor/composer/autoload_psr4.php 新文件: assets/opencc/vendor/composer/autoload_real.php 新文件: assets/opencc/vendor/composer/autoload_static.php 新文件: assets/opencc/vendor/composer/installed.json 新文件: assets/opencc/vendor/composer/installed.php 新文件: assets/opencc/vendor/composer/platform_check.php 新文件: assets/opencc/vendor/overtrue/php-opencc/.editorconfig 新文件: assets/opencc/vendor/overtrue/php-opencc/.github/FUNDING.yml 新文件: assets/opencc/vendor/overtrue/php-opencc/.github/workflows/test.yml 新文件: assets/opencc/vendor/overtrue/php-opencc/LICENSE 新文件: assets/opencc/vendor/overtrue/php-opencc/README.md 新文件: assets/opencc/vendor/overtrue/php-opencc/bin/build 新文件: assets/opencc/vendor/overtrue/php-opencc/bin/opencc 新文件: assets/opencc/vendor/overtrue/php-opencc/composer.json 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/HKVariants.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/HKVariantsRevPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/JPShinjitaiCharacters.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/JPShinjitaiPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/JPVariants.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/README.md 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/STCharacters.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/STPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TSCharacters.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TSPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWPhrasesIT.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWPhrasesName.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWPhrasesOther.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWVariants.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWVariantsRevPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/HKVariants.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/HKVariantsRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/HKVariantsRevPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPShinjitaiCharacters.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPShinjitaiPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPVariants.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPVariantsRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/STCharacters.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/STPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TSCharacters.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TSPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesIT.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesName.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesOther.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWVariants.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWVariantsRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWVariantsRevPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Console/BuildCommand.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Console/ConvertCommand.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Contracts/ConverterInterface.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Converter.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Dictionary.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/OpenCC.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Strategy.php 新文件: assets/opencc/vendor/psr/container/.gitignore 新文件: assets/opencc/vendor/psr/container/LICENSE 新文件: assets/opencc/vendor/psr/container/README.md 新文件: assets/opencc/vendor/psr/container/composer.json 新文件: assets/opencc/vendor/psr/container/src/ContainerExceptionInterface.php 新文件: assets/opencc/vendor/psr/container/src/ContainerInterface.php 新文件: assets/opencc/vendor/psr/container/src/NotFoundExceptionInterface.php 新文件: assets/opencc/vendor/symfony/console/Application.php 新文件: assets/opencc/vendor/symfony/console/Attribute/AsCommand.php 新文件: assets/opencc/vendor/symfony/console/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/console/CI/GithubActionReporter.php 新文件: assets/opencc/vendor/symfony/console/Color.php 新文件: assets/opencc/vendor/symfony/console/Command/Command.php 新文件: assets/opencc/vendor/symfony/console/Command/CompleteCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/DumpCompletionCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/HelpCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/LazyCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/ListCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/LockableTrait.php 新文件: assets/opencc/vendor/symfony/console/Command/SignalableCommandInterface.php 新文件: assets/opencc/vendor/symfony/console/Command/TraceableCommand.php 新文件: assets/opencc/vendor/symfony/console/CommandLoader/CommandLoaderInterface.php 新文件: assets/opencc/vendor/symfony/console/CommandLoader/ContainerCommandLoader.php 新文件: assets/opencc/vendor/symfony/console/CommandLoader/FactoryCommandLoader.php 新文件: assets/opencc/vendor/symfony/console/Completion/CompletionInput.php 新文件: assets/opencc/vendor/symfony/console/Completion/CompletionSuggestions.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/BashCompletionOutput.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/CompletionOutputInterface.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/FishCompletionOutput.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/ZshCompletionOutput.php 新文件: assets/opencc/vendor/symfony/console/Completion/Suggestion.php 新文件: assets/opencc/vendor/symfony/console/ConsoleEvents.php 新文件: assets/opencc/vendor/symfony/console/Cursor.php 新文件: assets/opencc/vendor/symfony/console/DataCollector/CommandDataCollector.php 新文件: assets/opencc/vendor/symfony/console/Debug/CliRequest.php 新文件: assets/opencc/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/ApplicationDescription.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/Descriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/DescriptorInterface.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/JsonDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/MarkdownDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/ReStructuredTextDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/TextDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/XmlDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleCommandEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleErrorEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleSignalEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleTerminateEvent.php 新文件: assets/opencc/vendor/symfony/console/EventListener/ErrorListener.php 新文件: assets/opencc/vendor/symfony/console/Exception/CommandNotFoundException.php 新文件: assets/opencc/vendor/symfony/console/Exception/ExceptionInterface.php 新文件: assets/opencc/vendor/symfony/console/Exception/InvalidArgumentException.php 新文件: assets/opencc/vendor/symfony/console/Exception/InvalidOptionException.php 新文件: assets/opencc/vendor/symfony/console/Exception/LogicException.php 新文件: assets/opencc/vendor/symfony/console/Exception/MissingInputException.php 新文件: assets/opencc/vendor/symfony/console/Exception/NamespaceNotFoundException.php 新文件: assets/opencc/vendor/symfony/console/Exception/RunCommandFailedException.php 新文件: assets/opencc/vendor/symfony/console/Exception/RuntimeException.php 新文件: assets/opencc/vendor/symfony/console/Formatter/NullOutputFormatter.php 新文件: assets/opencc/vendor/symfony/console/Formatter/NullOutputFormatterStyle.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatter.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterInterface.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterStyle.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterStyleStack.php 新文件: assets/opencc/vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php 新文件: assets/opencc/vendor/symfony/console/Helper/DebugFormatterHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/DescriptorHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/Dumper.php 新文件: assets/opencc/vendor/symfony/console/Helper/FormatterHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/Helper.php 新文件: assets/opencc/vendor/symfony/console/Helper/HelperInterface.php 新文件: assets/opencc/vendor/symfony/console/Helper/HelperSet.php 新文件: assets/opencc/vendor/symfony/console/Helper/InputAwareHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/OutputWrapper.php 新文件: assets/opencc/vendor/symfony/console/Helper/ProcessHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/ProgressBar.php 新文件: assets/opencc/vendor/symfony/console/Helper/ProgressIndicator.php 新文件: assets/opencc/vendor/symfony/console/Helper/QuestionHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/SymfonyQuestionHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/Table.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableCell.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableCellStyle.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableRows.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableSeparator.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableStyle.php 新文件: assets/opencc/vendor/symfony/console/Input/ArgvInput.php 新文件: assets/opencc/vendor/symfony/console/Input/ArrayInput.php 新文件: assets/opencc/vendor/symfony/console/Input/Input.php 新文件: assets/opencc/vendor/symfony/console/Input/InputArgument.php 新文件: assets/opencc/vendor/symfony/console/Input/InputAwareInterface.php 新文件: assets/opencc/vendor/symfony/console/Input/InputDefinition.php 新文件: assets/opencc/vendor/symfony/console/Input/InputInterface.php 新文件: assets/opencc/vendor/symfony/console/Input/InputOption.php 新文件: assets/opencc/vendor/symfony/console/Input/StreamableInputInterface.php 新文件: assets/opencc/vendor/symfony/console/Input/StringInput.php 新文件: assets/opencc/vendor/symfony/console/LICENSE 新文件: assets/opencc/vendor/symfony/console/Logger/ConsoleLogger.php 新文件: assets/opencc/vendor/symfony/console/Messenger/RunCommandContext.php 新文件: assets/opencc/vendor/symfony/console/Messenger/RunCommandMessage.php 新文件: assets/opencc/vendor/symfony/console/Messenger/RunCommandMessageHandler.php 新文件: assets/opencc/vendor/symfony/console/Output/AnsiColorMode.php 新文件: assets/opencc/vendor/symfony/console/Output/BufferedOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/ConsoleOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/ConsoleOutputInterface.php 新文件: assets/opencc/vendor/symfony/console/Output/ConsoleSectionOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/NullOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/Output.php 新文件: assets/opencc/vendor/symfony/console/Output/OutputInterface.php 新文件: assets/opencc/vendor/symfony/console/Output/StreamOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/TrimmedBufferOutput.php 新文件: assets/opencc/vendor/symfony/console/Question/ChoiceQuestion.php 新文件: assets/opencc/vendor/symfony/console/Question/ConfirmationQuestion.php 新文件: assets/opencc/vendor/symfony/console/Question/Question.php 新文件: assets/opencc/vendor/symfony/console/README.md 新文件: assets/opencc/vendor/symfony/console/Resources/bin/hiddeninput.exe 新文件: assets/opencc/vendor/symfony/console/Resources/completion.bash 新文件: assets/opencc/vendor/symfony/console/Resources/completion.fish 新文件: assets/opencc/vendor/symfony/console/Resources/completion.zsh 新文件: assets/opencc/vendor/symfony/console/SignalRegistry/SignalMap.php 新文件: assets/opencc/vendor/symfony/console/SignalRegistry/SignalRegistry.php 新文件: assets/opencc/vendor/symfony/console/SingleCommandApplication.php 新文件: assets/opencc/vendor/symfony/console/Style/OutputStyle.php 新文件: assets/opencc/vendor/symfony/console/Style/StyleInterface.php 新文件: assets/opencc/vendor/symfony/console/Style/SymfonyStyle.php 新文件: assets/opencc/vendor/symfony/console/Terminal.php 新文件: assets/opencc/vendor/symfony/console/Tester/ApplicationTester.php 新文件: assets/opencc/vendor/symfony/console/Tester/CommandCompletionTester.php 新文件: assets/opencc/vendor/symfony/console/Tester/CommandTester.php 新文件: assets/opencc/vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php 新文件: assets/opencc/vendor/symfony/console/Tester/TesterTrait.php 新文件: assets/opencc/vendor/symfony/console/composer.json 新文件: assets/opencc/vendor/symfony/deprecation-contracts/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/deprecation-contracts/LICENSE 新文件: assets/opencc/vendor/symfony/deprecation-contracts/README.md 新文件: assets/opencc/vendor/symfony/deprecation-contracts/composer.json 新文件: assets/opencc/vendor/symfony/deprecation-contracts/function.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/Ctype.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-ctype/README.md 新文件: assets/opencc/vendor/symfony/polyfill-ctype/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/composer.json 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/Grapheme.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/README.md 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/composer.json 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Normalizer.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/README.md 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/composer.json 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Mbstring.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/README.md 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/composer.json 新文件: assets/opencc/vendor/symfony/process/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/process/Exception/ExceptionInterface.php 新文件: assets/opencc/vendor/symfony/process/Exception/InvalidArgumentException.php 新文件: assets/opencc/vendor/symfony/process/Exception/LogicException.php 新文件: assets/opencc/vendor/symfony/process/Exception/ProcessFailedException.php 新文件: assets/opencc/vendor/symfony/process/Exception/ProcessSignaledException.php 新文件: assets/opencc/vendor/symfony/process/Exception/ProcessTimedOutException.php 新文件: assets/opencc/vendor/symfony/process/Exception/RunProcessFailedException.php 新文件: assets/opencc/vendor/symfony/process/Exception/RuntimeException.php 新文件: assets/opencc/vendor/symfony/process/ExecutableFinder.php 新文件: assets/opencc/vendor/symfony/process/InputStream.php 新文件: assets/opencc/vendor/symfony/process/LICENSE 新文件: assets/opencc/vendor/symfony/process/Messenger/RunProcessContext.php 新文件: assets/opencc/vendor/symfony/process/Messenger/RunProcessMessage.php 新文件: assets/opencc/vendor/symfony/process/Messenger/RunProcessMessageHandler.php 新文件: assets/opencc/vendor/symfony/process/PhpExecutableFinder.php 新文件: assets/opencc/vendor/symfony/process/PhpProcess.php 新文件: assets/opencc/vendor/symfony/process/PhpSubprocess.php 新文件: assets/opencc/vendor/symfony/process/Pipes/AbstractPipes.php 新文件: assets/opencc/vendor/symfony/process/Pipes/PipesInterface.php 新文件: assets/opencc/vendor/symfony/process/Pipes/UnixPipes.php 新文件: assets/opencc/vendor/symfony/process/Pipes/WindowsPipes.php 新文件: assets/opencc/vendor/symfony/process/Process.php 新文件: assets/opencc/vendor/symfony/process/ProcessUtils.php 新文件: assets/opencc/vendor/symfony/process/README.md 新文件: assets/opencc/vendor/symfony/process/composer.json 新文件: assets/opencc/vendor/symfony/service-contracts/Attribute/Required.php 新文件: assets/opencc/vendor/symfony/service-contracts/Attribute/SubscribedService.php 新文件: assets/opencc/vendor/symfony/service-contracts/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/service-contracts/LICENSE 新文件: assets/opencc/vendor/symfony/service-contracts/README.md 新文件: assets/opencc/vendor/symfony/service-contracts/ResetInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceCollectionInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceLocatorTrait.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceMethodsSubscriberTrait.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceProviderInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceSubscriberInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceSubscriberTrait.php 新文件: assets/opencc/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php 新文件: assets/opencc/vendor/symfony/service-contracts/Test/ServiceLocatorTestCase.php 新文件: assets/opencc/vendor/symfony/service-contracts/composer.json 新文件: assets/opencc/vendor/symfony/string/AbstractString.php 新文件: assets/opencc/vendor/symfony/string/AbstractUnicodeString.php 新文件: assets/opencc/vendor/symfony/string/ByteString.php 新文件: assets/opencc/vendor/symfony/string/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/string/CodePointString.php 新文件: assets/opencc/vendor/symfony/string/Exception/ExceptionInterface.php 新文件: assets/opencc/vendor/symfony/string/Exception/InvalidArgumentException.php 新文件: assets/opencc/vendor/symfony/string/Exception/RuntimeException.php 新文件: assets/opencc/vendor/symfony/string/Inflector/EnglishInflector.php 新文件: assets/opencc/vendor/symfony/string/Inflector/FrenchInflector.php 新文件: assets/opencc/vendor/symfony/string/Inflector/InflectorInterface.php 新文件: assets/opencc/vendor/symfony/string/LICENSE 新文件: assets/opencc/vendor/symfony/string/LazyString.php 新文件: assets/opencc/vendor/symfony/string/README.md 新文件: assets/opencc/vendor/symfony/string/Resources/data/wcswidth_table_wide.php 新文件: assets/opencc/vendor/symfony/string/Resources/data/wcswidth_table_zero.php 新文件: assets/opencc/vendor/symfony/string/Resources/functions.php 新文件: assets/opencc/vendor/symfony/string/Slugger/AsciiSlugger.php 新文件: assets/opencc/vendor/symfony/string/Slugger/SluggerInterface.php 新文件: assets/opencc/vendor/symfony/string/UnicodeString.php 新文件: assets/opencc/vendor/symfony/string/composer.json 新文件: assets/phpliteadmin.php 新文件: config/Caddyfile 新文件: config/nginx.conf 新文件: cron.php 新文件: cron/requirements.txt 新文件: cron/update.py 新文件: index.php 新文件: manage.php 新文件: public.php 新文件: update.php
1380 lines
56 KiB
JavaScript
1380 lines
56 KiB
JavaScript
// 页面加载时预加载数据,减少等待时间
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
showModal('live', popup = false);
|
||
showModal('channel', popup = false);
|
||
showModal('update', popup = false);
|
||
showVersionLog(doCheckUpdate = true);
|
||
});
|
||
|
||
// 提交配置表单
|
||
document.getElementById('settingsForm').addEventListener('submit', function (event) {
|
||
event.preventDefault(); // 阻止默认表单提交
|
||
|
||
const fields = ['update_config', 'gen_xml', 'include_future_only', 'ret_default', 'all_chs',
|
||
'db_type', 'mysql_host', 'mysql_dbname', 'mysql_username', 'mysql_password', 'gen_list_enable',
|
||
'check_update', 'token_range', 'user_agent_range', 'live_template_enable', 'live_fuzzy_match',
|
||
'live_url_comment', 'live_tvg_logo_enable', 'live_tvg_id_enable', 'live_tvg_name_enable'];
|
||
|
||
// 创建隐藏字段并将其添加到表单
|
||
const form = this;
|
||
fields.forEach(field => {
|
||
const hiddenInput = document.createElement('input');
|
||
hiddenInput.type = 'hidden';
|
||
hiddenInput.name = field;
|
||
hiddenInput.value = document.getElementById(field).value;
|
||
form.appendChild(hiddenInput);
|
||
});
|
||
|
||
// 获取表单数据
|
||
const formData = new FormData(form);
|
||
|
||
// 执行 fetch 请求
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const { db_type_set, interval_time, start_time, end_time } = data;
|
||
|
||
let message = '配置已更新<br><br>';
|
||
if (!db_type_set) {
|
||
message += 'MySQL 启用失败<br>数据库已设为 SQLite<br><br>';
|
||
document.getElementById('db_type').value = 'sqlite';
|
||
updateMySQLFields();
|
||
}
|
||
message += interval_time === 0
|
||
? "已取消定时任务"
|
||
: `已设置定时任务<br>开始时间:${start_time}<br>结束时间:${end_time}<br>间隔周期:${formatTime(interval_time)}`;
|
||
|
||
showMessageModal(message);
|
||
})
|
||
.catch(() => showMessageModal('发生错误,请重试。'));
|
||
});
|
||
|
||
// 保存配置
|
||
function updateConfig() {
|
||
document.getElementById('update_config').click();
|
||
}
|
||
|
||
// 检查数据库状况
|
||
// 这个功能只是为了快速跳转到 PMA,用户可以自行修改路径测试。
|
||
function handleDbManagement() {
|
||
if (document.getElementById('db_type').value === 'mysql') {
|
||
var img = new Image();
|
||
var timeout = setTimeout(function () { img.onerror(); }, 1000); // 设置 1 秒超时
|
||
img.onload = function () {
|
||
clearTimeout(timeout); // 清除超时
|
||
window.open('http://' + window.location.hostname + ':8080', '_blank');
|
||
};
|
||
img.onerror = function () {
|
||
clearTimeout(timeout); // 清除超时
|
||
showMessageModal('无法访问 phpMyAdmin 8080 端口,请自行使用 MySQL 管理工具进行管理。');
|
||
};
|
||
img.src = 'http://' + window.location.hostname + ':8080/favicon.ico'; // 测试 8080 端口
|
||
return false;
|
||
}
|
||
return true; // 如果不是 MySQL,正常跳转
|
||
}
|
||
|
||
// 退出登录
|
||
function logout() {
|
||
// 清除所有cookies
|
||
document.cookie.split(";").forEach(function (cookie) {
|
||
var name = cookie.split("=")[0].trim();
|
||
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';
|
||
});
|
||
// 清除本地存储
|
||
sessionStorage.clear();
|
||
// 重定向到登录页面
|
||
window.location.href = 'manage.php';
|
||
}
|
||
|
||
// Ctrl+S 保存设置
|
||
document.addEventListener("keydown", function (event) {
|
||
if (event.ctrlKey && event.key === "s") {
|
||
event.preventDefault(); // 阻止默认行为,如保存页面
|
||
setGenListAndUpdateConfig();
|
||
}
|
||
});
|
||
|
||
// Ctrl+/ 设置(取消)注释
|
||
document.getElementById('xml_urls').addEventListener('keydown', handleKeydown);
|
||
document.getElementById('sourceUrlTextarea').addEventListener('keydown', handleKeydown);
|
||
function handleKeydown(event) {
|
||
if (event.ctrlKey && event.key === '/') {
|
||
event.preventDefault();
|
||
const textarea = this;
|
||
const { selectionStart, selectionEnd, value } = textarea;
|
||
const lines = value.split('\n');
|
||
// 计算当前选中的行
|
||
const startLine = value.slice(0, selectionStart).split('\n').length - 1;
|
||
const endLine = value.slice(0, selectionEnd).split('\n').length - 1;
|
||
// 判断选中的行是否都已注释
|
||
const allCommented = lines.slice(startLine, endLine + 1).every(line => line.trim().startsWith('#'));
|
||
const newLines = lines.map((line, index) => {
|
||
if (index >= startLine && index <= endLine) {
|
||
return allCommented ? line.replace(/^#\s*/, '') : '# ' + line;
|
||
}
|
||
return line;
|
||
});
|
||
// 更新 textarea 的内容
|
||
textarea.value = newLines.join('\n');
|
||
// 检查光标开始位置是否在行首
|
||
const startLineStartIndex = value.lastIndexOf('\n', selectionStart - 1) + 1;
|
||
const isStartInLineStart = (selectionStart - startLineStartIndex < 2);
|
||
// 检查光标结束位置是否在行首
|
||
const endLineStartIndex = value.lastIndexOf('\n', selectionEnd - 1) + 1;
|
||
const isEndInLineStart = (selectionEnd - endLineStartIndex < 2);
|
||
// 计算光标新的开始位置
|
||
const newSelectionStart = isStartInLineStart
|
||
? startLineStartIndex
|
||
: selectionStart + newLines[startLine].length - lines[startLine].length;
|
||
// 计算光标新的结束位置
|
||
const lengthDiff = newLines.join('').length - lines.join('').length;
|
||
const endLineDiff = newLines[endLine].length - lines[endLine].length;
|
||
const newSelectionEnd = isEndInLineStart
|
||
? (endLineDiff > 0 ? endLineStartIndex + lengthDiff : endLineStartIndex + lengthDiff - endLineDiff)
|
||
: selectionEnd + lengthDiff;
|
||
// 恢复光标位置
|
||
textarea.setSelectionRange(newSelectionStart, newSelectionEnd);
|
||
}
|
||
}
|
||
|
||
// 格式化时间
|
||
function formatTime(seconds) {
|
||
const formattedHours = String(Math.floor(seconds / 3600));
|
||
const formattedMinutes = String(Math.floor((seconds % 3600) / 60));
|
||
return `${formattedHours}小时${formattedMinutes}分钟`;
|
||
}
|
||
|
||
// 更新 MySQL 按钮状态
|
||
function updateMySQLFields() {
|
||
var dbType = document.getElementById('db_type').value;
|
||
var isSQLite = (dbType === 'sqlite');
|
||
document.getElementById('mysql_host').disabled = isSQLite;
|
||
document.getElementById('mysql_dbname').disabled = isSQLite;
|
||
document.getElementById('mysql_username').disabled = isSQLite;
|
||
document.getElementById('mysql_password').disabled = isSQLite;
|
||
}
|
||
|
||
// 显示带消息的模态框
|
||
function showModalWithMessage(modalId, messageId = '', message = '') {
|
||
const modal = document.getElementById(modalId);
|
||
if (messageId) document.getElementById(messageId).innerHTML = message;
|
||
|
||
modal.style.zIndex = zIndex++;
|
||
modal.style.display = "block";
|
||
|
||
const closeBtn = modal.querySelector(".close");
|
||
closeBtn.onmousedown = () => modal.style.display = "none";
|
||
|
||
// 处理点击模态框外部关闭
|
||
const handleClickOutside = (event) => {
|
||
if (event.target === modal) {
|
||
modal.style.display = "none";
|
||
window.removeEventListener('mousedown', handleClickOutside); // 关闭后移除事件监听器
|
||
}
|
||
};
|
||
|
||
window.addEventListener('mousedown', handleClickOutside);
|
||
|
||
// 阻止点击模态框内部时关闭
|
||
modal.querySelector('.modal-content').addEventListener('mousedown', (e) => e.stopPropagation());
|
||
}
|
||
|
||
// 显示消息模态框
|
||
function showMessageModal(message) {
|
||
showModalWithMessage("messageModal", "messageModalMessage", message);
|
||
}
|
||
|
||
let zIndex = 100;
|
||
// 显示模态框公共函数
|
||
function showModal(type, popup = true, data = '') {
|
||
var modal, logSpan, logContent;
|
||
switch (type) {
|
||
case 'epg':
|
||
modal = document.getElementById("epgModal");
|
||
fetchData("manage.php?get_epg_by_channel=true&channel=" + encodeURIComponent(data.channel) + "&date=" + data.date, updateEpgContent);
|
||
|
||
// 更新日期的点击事件
|
||
const updateDate = function (offset) {
|
||
const currentDate = new Date(document.getElementById("epgDate").innerText);
|
||
currentDate.setDate(currentDate.getDate() + offset);
|
||
const newDateString = currentDate.toISOString().split('T')[0];
|
||
fetchData(`manage.php?get_epg_by_channel=true&channel=${encodeURIComponent(data.channel)}&date=${newDateString}`, updateEpgContent);
|
||
document.getElementById("epgDate").innerText = newDateString;
|
||
};
|
||
|
||
// 前一天和后一天的点击事件
|
||
document.getElementById('prevDate').onclick = () => updateDate(-1);
|
||
document.getElementById('nextDate').onclick = () => updateDate(1);
|
||
|
||
break;
|
||
|
||
case 'update':
|
||
modal = document.getElementById("updatelogModal");
|
||
fetchData('manage.php?get_update_logs=true', updateLogTable);
|
||
break;
|
||
case 'cron':
|
||
modal = document.getElementById("cronlogModal");
|
||
fetchData('manage.php?get_cron_logs=true', updateCronLogContent);
|
||
break;
|
||
case 'channel':
|
||
modal = document.getElementById("channelModal");
|
||
fetchData('manage.php?get_channel=true', updateChannelList);
|
||
break;
|
||
case 'icon':
|
||
modal = document.getElementById("iconModal");
|
||
fetchData('manage.php?get_icon=true', updateIconList);
|
||
break;
|
||
case 'allicon':
|
||
modal = document.getElementById("iconModal");
|
||
fetchData('manage.php?get_icon=true&get_all_icon=true', updateIconList);
|
||
break;
|
||
case 'channelbindepg':
|
||
modal = document.getElementById("channelBindEPGModal");
|
||
fetchData('manage.php?get_channel_bind_epg=true', updateChannelBindEPGList);
|
||
break;
|
||
case 'channelmatch':
|
||
modal = document.getElementById("channelMatchModal");
|
||
fetchData('manage.php?get_channel_match=true', updateChannelMatchList);
|
||
break;
|
||
case 'live':
|
||
modal = document.getElementById("liveSourceManageModal");
|
||
fetchData('manage.php?get_live_data=true', updateLiveSourceModal);
|
||
break;
|
||
case 'moresetting':
|
||
updateMySQLFields(); // 设置 MySQL 相关输入框状态
|
||
document.getElementById('db_type').addEventListener('change', updateMySQLFields);
|
||
modal = document.getElementById("moreSettingModal");
|
||
fetchData('manage.php?get_gen_list=true', updateGenList);
|
||
break;
|
||
default:
|
||
console.error('Unknown type:', type);
|
||
break;
|
||
}
|
||
if (!popup) {
|
||
return;
|
||
}
|
||
modal.style.zIndex = zIndex++; // 确保 modal 在最上层
|
||
modal.style.display = "block";
|
||
|
||
var originalOnMouseDown = window.onmousedown;
|
||
function handleModalClose() {
|
||
modal.style.display = "none";
|
||
window.onmousedown = originalOnMouseDown; // 恢复原事件
|
||
}
|
||
|
||
closeBtn = modal.querySelector(".close");
|
||
closeBtn.onmousedown = handleModalClose;
|
||
window.onmousedown = function (event) {
|
||
if (event.target === modal) {
|
||
handleModalClose();
|
||
}
|
||
}
|
||
}
|
||
|
||
function fetchData(endpoint, callback) {
|
||
fetch(endpoint)
|
||
.then(response => response.json())
|
||
.then(data => callback(data))
|
||
.catch(error => {
|
||
console.error('Error fetching log:', error);
|
||
callback([]);
|
||
});
|
||
}
|
||
|
||
// 显示 update.php、check.php 执行结果
|
||
function showExecResult(fileName, callback, fullSize = true) {
|
||
|
||
showMessageModal('');
|
||
const messageContainer = document.getElementById('messageModalMessage');
|
||
|
||
// 清空 messageContainer,避免内容重复
|
||
messageContainer.innerHTML = '';
|
||
|
||
const wrapper = document.createElement('div');
|
||
if (fullSize) {
|
||
wrapper.style.width = '800px';
|
||
wrapper.style.height = '500px';
|
||
}
|
||
wrapper.style.overflow = 'auto';
|
||
messageContainer.appendChild(wrapper);
|
||
|
||
// 创建 XMLHttpRequest 对象
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('GET', `${fileName}`, true);
|
||
|
||
// 显式设置 X-Requested-With 请求头
|
||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||
|
||
// 处理接收到的数据
|
||
xhr.onprogress = function () {
|
||
wrapper.innerHTML = xhr.responseText;
|
||
wrapper.scrollTop = wrapper.scrollHeight;
|
||
};
|
||
|
||
xhr.onload = function () {
|
||
if (xhr.status === 200) {
|
||
// 确保执行完成后调用回调
|
||
if (typeof callback === 'function') {
|
||
callback();
|
||
}
|
||
} else {
|
||
wrapper.innerHTML += '<p>检测失败,请检查服务器。</p>';
|
||
}
|
||
};
|
||
|
||
xhr.onerror = function () {
|
||
wrapper.innerHTML += '<p>请求出错,请检查网络连接。</p>';
|
||
};
|
||
|
||
xhr.send();
|
||
}
|
||
|
||
// 显示版本更新日志
|
||
function showVersionLog(doCheckUpdate = false) {
|
||
fetch(`manage.php?get_version_log=true&do_check_update=${doCheckUpdate}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
if (!doCheckUpdate || data.is_updated) {
|
||
showModalWithMessage("versionLogModal", "versionLogMessage", data.content);
|
||
}
|
||
} else {
|
||
showMessageModal(data.message || '获取版本日志失败');
|
||
}
|
||
})
|
||
.catch(() => {
|
||
showMessageModal('无法获取版本日志,请稍后重试');
|
||
});
|
||
}
|
||
|
||
// 显示使用说明
|
||
function showHelpModal() {
|
||
showModalWithMessage("helpModal");
|
||
}
|
||
|
||
// 更新 EPG 内容
|
||
function updateEpgContent(epgData) {
|
||
document.getElementById('epgTitle').innerHTML = epgData.channel;
|
||
document.getElementById('epgSource').innerHTML = `来源:${epgData.source}`;
|
||
document.getElementById('epgDate').innerHTML = epgData.date;
|
||
var epgContent = document.getElementById("epgContent");
|
||
epgContent.value = epgData.epg;
|
||
epgContent.scrollTop = 0;
|
||
}
|
||
|
||
// 更新日志表格
|
||
function updateLogTable(logData) {
|
||
var logTableBody = document.querySelector("#logTable tbody");
|
||
logTableBody.innerHTML = '';
|
||
|
||
logData.forEach(log => {
|
||
var row = document.createElement("tr");
|
||
row.innerHTML = `
|
||
<td>${new Date(log.timestamp).toLocaleString('zh-CN').replace(' ', '<br>')}</td>
|
||
<td>${log.log_message}</td>
|
||
`;
|
||
logTableBody.appendChild(row);
|
||
});
|
||
var logTableContainer = document.getElementById("log-table-container");
|
||
logTableContainer.scrollTop = logTableContainer.scrollHeight;
|
||
}
|
||
|
||
// 更新 cron 日志内容
|
||
function updateCronLogContent(logData) {
|
||
var logContent = document.getElementById("cronLogContent");
|
||
logContent.value = logData.map(log =>
|
||
`[${new Date(log.timestamp).toLocaleString('zh-CN', {
|
||
month: '2-digit', day: '2-digit',
|
||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||
hour12: false
|
||
})}] ${log.log_message}`)
|
||
.join('\n');
|
||
logContent.scrollTop = logContent.scrollHeight;
|
||
}
|
||
|
||
// 显示频道别名列表
|
||
function updateChannelList(channelsData) {
|
||
const channelTitle = document.getElementById('channelModalTitle');
|
||
channelTitle.innerHTML = `频道列表<span style="font-size: 18px;">(总数:${channelsData.count})</span>`; // 更新频道总数
|
||
document.getElementById('channelTable').dataset.allChannels = JSON.stringify(channelsData.channels); // 将原始频道和映射后的频道数据存储到 dataset 中
|
||
filterChannels('channel'); // 生成数据
|
||
}
|
||
|
||
// 显示台标列表
|
||
function updateIconList(iconsData) {
|
||
const channelTitle = document.getElementById('iconModalTitle');
|
||
channelTitle.innerHTML = `频道列表<span style="font-size: 18px;">(总数:${iconsData.count})</span>`; // 更新频道总数
|
||
document.getElementById('iconTable').dataset.allIcons = JSON.stringify(iconsData.channels); // 将频道名和台标地址存储到 dataset 中
|
||
filterChannels('icon'); // 生成数据
|
||
}
|
||
|
||
// 显示频道绑定 EPG 列表
|
||
function updateChannelBindEPGList(channelBindEPGData) {
|
||
// 创建并添加隐藏字段
|
||
const channelBindEPGInput = document.createElement('input');
|
||
channelBindEPGInput.type = 'hidden';
|
||
channelBindEPGInput.name = 'channel_bind_epg';
|
||
document.getElementById('settingsForm').appendChild(channelBindEPGInput);
|
||
|
||
document.getElementById('channelBindEPGTable').dataset.allChannelBindEPG = JSON.stringify(channelBindEPGData);
|
||
var channelBindEPGTableBody = document.querySelector("#channelBindEPGTable tbody");
|
||
var allChannelBindEPG = JSON.parse(document.getElementById('channelBindEPGTable').dataset.allChannelBindEPG);
|
||
channelBindEPGInput.value = JSON.stringify(allChannelBindEPG);
|
||
|
||
// 清空现有表格
|
||
channelBindEPGTableBody.innerHTML = '';
|
||
|
||
allChannelBindEPG.forEach(channelbindepg => {
|
||
var row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${String(channelbindepg.epg_src)}</td>
|
||
<td contenteditable="true">${channelbindepg.channels}</td>
|
||
`;
|
||
|
||
row.querySelector('td[contenteditable]').addEventListener('input', function () {
|
||
channelbindepg.channels = this.textContent;
|
||
document.getElementById('channelBindEPGTable').dataset.allChannelBindEPG = JSON.stringify(allChannelBindEPG);
|
||
channelBindEPGInput.value = JSON.stringify(allChannelBindEPG);
|
||
});
|
||
|
||
channelBindEPGTableBody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// 显示频道匹配结果
|
||
function updateChannelMatchList(channelMatchdata) {
|
||
const channelMatchTableBody = document.querySelector("#channelMatchTable tbody");
|
||
channelMatchTableBody.innerHTML = '';
|
||
|
||
const typeOrder = { '未匹配': 1, '反向模糊': 2, '正向模糊': 3, '别名/忽略': 4, '精确匹配': 5 };
|
||
|
||
// 处理并排序匹配数据
|
||
const sortedMatches = Object.values(channelMatchdata)
|
||
.flat()
|
||
.sort((a, b) => typeOrder[a.type] - typeOrder[b.type]);
|
||
|
||
// 创建表格行
|
||
sortedMatches.forEach(({ ori_channel, clean_channel, match, type }) => {
|
||
const matchType = type === '精确匹配' ? '' : type;
|
||
const row = document.createElement("tr");
|
||
row.innerHTML = `
|
||
<td>${ori_channel}</td>
|
||
<td>${clean_channel}</td>
|
||
<td>${match || ''}</td>
|
||
<td>${matchType}</td>
|
||
`;
|
||
channelMatchTableBody.appendChild(row);
|
||
});
|
||
|
||
document.getElementById("channel-match-table-container").style.display = 'block';
|
||
}
|
||
|
||
// 显示限定频道列表
|
||
function updateGenList(genData) {
|
||
const gen_list_text = document.getElementById('gen_list_text');
|
||
if (!gen_list_text.value) {
|
||
gen_list_text.value = genData.join('\n');
|
||
}
|
||
}
|
||
|
||
// 显示指定页码的数据
|
||
function displayPage(data, page) {
|
||
const tableBody = document.querySelector('#liveSourceTable tbody');
|
||
tableBody.innerHTML = ''; // 清空表格内容
|
||
|
||
const start = (page - 1) * rowsPerPage;
|
||
const end = Math.min(start + rowsPerPage, data.length);
|
||
|
||
if (data.length === 0) {
|
||
tableBody.innerHTML = '<tr><td colspan="11">暂无数据</td></tr>';
|
||
return;
|
||
}
|
||
|
||
// 列索引和对应字段的映射
|
||
const columns = ['groupTitle', 'channelName', 'streamUrl', 'iconUrl', 'tvgId',
|
||
'tvgName', 'resolution', 'speed', 'disable', 'modified'];
|
||
|
||
// 填充当前页的表格数据
|
||
data.slice(start, end).forEach((item, index) => {
|
||
const row = document.createElement('tr');
|
||
|
||
row.innerHTML = `
|
||
<td>${start + index + 1}</td>
|
||
${columns.map((col, columnIndex) => {
|
||
let cellContent = item[col] || '';
|
||
let cellClass = '';
|
||
|
||
// 处理 disable 和 modified 列
|
||
if (col === 'disable' || col === 'modified') {
|
||
cellContent = item[col] == 1 ? '是' : '否';
|
||
cellClass = (col === 'disable' && item[col] == 1)
|
||
? 'table-cell-disable'
|
||
: (col === 'modified' && item[col] == 1)
|
||
? 'table-cell-modified'
|
||
: 'table-cell-clickable';
|
||
}
|
||
|
||
const editable = ['resolution', 'speed', 'disable', 'modified'].includes(col) ? '' : 'contenteditable="true"';
|
||
const clickableClass = (col === 'disable' || col === 'modified') ? 'table-cell-clickable' : '';
|
||
|
||
return `<td ${editable} class="${clickableClass} ${cellClass}">
|
||
${cellContent}
|
||
</td>`;
|
||
}).join('')}
|
||
`;
|
||
|
||
// 为每个单元格添加事件监听器
|
||
row.querySelectorAll('td[contenteditable="true"]').forEach((cell, columnIndex) => {
|
||
cell.addEventListener('input', () => {
|
||
const dataIndex = (currentPage - 1) * rowsPerPage + index;
|
||
if (dataIndex < allLiveData.length) {
|
||
allLiveData[dataIndex][columns[columnIndex]] = cell.textContent.trim();
|
||
|
||
allLiveData[dataIndex]['modified'] = 1; // 标记修改位
|
||
const lastCell = cell.closest('tr').lastElementChild;
|
||
lastCell.textContent = '是';
|
||
lastCell.classList.add('table-cell-modified');
|
||
}
|
||
});
|
||
});
|
||
|
||
// 为 disable 和 modified 列添加点击事件,切换 "是/否"
|
||
row.querySelectorAll('td.table-cell-clickable').forEach((cell, columnIndex) => {
|
||
cell.addEventListener('click', () => {
|
||
const dataIndex = (currentPage - 1) * rowsPerPage + index;
|
||
if (dataIndex < allLiveData.length) {
|
||
const isDisable = columnIndex === 0;
|
||
const field = isDisable ? 'disable' : 'modified';
|
||
const newValue = allLiveData[dataIndex][field] == 1 ? 0 : 1;
|
||
allLiveData[dataIndex][field] = newValue;
|
||
cell.textContent = newValue == 1 ? '是' : '否';
|
||
|
||
if (isDisable) {
|
||
cell.classList.toggle('table-cell-disable', newValue == 1);
|
||
allLiveData[dataIndex]['modified'] = 1; // 标记修改位
|
||
const lastCell = cell.closest('tr').lastElementChild;
|
||
lastCell.textContent = '是';
|
||
lastCell.classList.add('table-cell-modified');
|
||
} else {
|
||
cell.classList.toggle('table-cell-modified', newValue == 1);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
tableBody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// 创建分页控件
|
||
function setupPagination(data) {
|
||
const paginationContainer = document.getElementById('paginationContainer');
|
||
paginationContainer.innerHTML = ''; // 清空分页容器
|
||
|
||
const totalPages = Math.ceil(data.length / rowsPerPage);
|
||
document.getElementById('live-source-table-container').style.height = totalPages <= 1 ? "410px" : "375px";
|
||
if (totalPages <= 1) return;
|
||
|
||
const maxButtons = 11; // 总显示按钮数,包括“<”和“>”
|
||
const pageButtons = maxButtons - 2; // 除去 "<" 和 ">" 的按钮数
|
||
|
||
// 创建按钮
|
||
const createButton = (text, page, isActive = false, isDisabled = false) => {
|
||
const button = document.createElement('button');
|
||
button.textContent = text;
|
||
button.className = isActive ? 'active' : '';
|
||
button.disabled = isDisabled;
|
||
button.onclick = () => {
|
||
if (!isDisabled) {
|
||
currentPage = page;
|
||
displayPage(data, currentPage); // 更新页面显示内容
|
||
setupPagination(data); // 更新分页控件
|
||
}
|
||
};
|
||
return button;
|
||
};
|
||
|
||
// 前部
|
||
paginationContainer.appendChild(createButton('<', currentPage - 1, false, currentPage === 1));
|
||
paginationContainer.appendChild(createButton(1, 1, currentPage === 1));
|
||
if (currentPage > 5 && totalPages > pageButtons) paginationContainer.appendChild(createButton('...', null, false, true));
|
||
|
||
// 中部
|
||
let startPage = Math.max(2, currentPage - Math.floor(pageButtons / 2) + 2);
|
||
let endPage = Math.min(totalPages - 1, currentPage + Math.floor(pageButtons / 2) - 2);
|
||
if (currentPage <= 5) { startPage = 2; endPage = Math.min(pageButtons - 2, totalPages - 1); }
|
||
else if (currentPage >= totalPages - 4) { startPage = Math.max(totalPages - pageButtons + 3, 2); endPage = totalPages - 1; }
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
paginationContainer.appendChild(createButton(i, i, currentPage === i));
|
||
}
|
||
|
||
// 后部
|
||
if (currentPage < totalPages - 4 && totalPages > pageButtons) paginationContainer.appendChild(createButton('...', null, false, true));
|
||
paginationContainer.appendChild(createButton(totalPages, totalPages, currentPage === totalPages));
|
||
paginationContainer.appendChild(createButton('>', currentPage + 1, false, currentPage === totalPages));
|
||
}
|
||
|
||
let currentPage = 1; // 当前页码
|
||
const rowsPerPage = 100; // 每页显示的行数
|
||
let allLiveData = []; // 用于存储直播源数据
|
||
|
||
// 更新模态框内容并初始化分页
|
||
function updateLiveSourceModal(data) {
|
||
document.getElementById('sourceUrlTextarea').value = data.source_content || '';
|
||
document.getElementById('liveTemplateTextarea').value = data.template_content || '';
|
||
const channels = Array.isArray(data.channels) ? data.channels : [];
|
||
allLiveData = channels; // 将所有数据保存在全局变量中
|
||
currentPage = 1; // 重置为第一页
|
||
displayPage(channels, currentPage); // 显示第一页数据
|
||
setupPagination(channels); // 初始化分页控件
|
||
}
|
||
|
||
// 上传直播源文件
|
||
document.getElementById('liveSourceFile').addEventListener('change', function () {
|
||
const file = this.files[0];
|
||
const allowedExtensions = ['m3u', 'txt'];
|
||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||
|
||
// 检查文件类型
|
||
if (!allowedExtensions.includes(fileExtension)) {
|
||
showMessageModal('只接受 .m3u 和 .txt 文件');
|
||
return;
|
||
}
|
||
|
||
// 创建 FormData 并发送 AJAX 请求
|
||
const formData = new FormData();
|
||
formData.append('liveSourceFile', file);
|
||
|
||
fetch('manage.php', { method: 'POST', body: formData })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showModal('live');
|
||
} else {
|
||
showMessageModal('上传失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => showMessageModal('上传过程中发生错误:' + error));
|
||
|
||
this.value = ''; // 重置文件输入框的值,确保可以连续上传相同文件
|
||
});
|
||
|
||
// 设置直播源自动同步、优化频道名开关
|
||
function toggleStatus(toggleBtn) {
|
||
fetch(`manage.php?toggle_status=true&toggle_button=${toggleBtn}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// 更新按钮显示
|
||
document.getElementById(toggleBtn).innerHTML =
|
||
`${toggleBtn === "toggleLiveSourceSyncBtn" ? "同步更新" : "频道更名"}: ${data.status === 1 ? "是" : "否"}`;
|
||
const syncStatus = document.getElementById("toggleLiveSourceSyncBtn").innerHTML;
|
||
const processStatus = document.getElementById("toggleLiveChannelNameProcessBtn").innerHTML;
|
||
document.getElementById('showMoreLiveSettingBtn').setAttribute('onclick', `showMoreLiveSetting('${syncStatus}', '${processStatus}')`);
|
||
})
|
||
.catch(error => console.error("Error:", error));
|
||
}
|
||
|
||
// 保存编辑后的直播源地址
|
||
function saveLiveSourceFile() {
|
||
source = document.getElementById('sourceUrlTextarea');
|
||
const sourceContent = source.value.replace(/^\s*[\r\n]+/gm, '').replace(/\n$/, '');
|
||
source.value = sourceContent;
|
||
|
||
// 内容写入 source.txt 文件
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({
|
||
save_content_to_file: 'true',
|
||
file_path: '/data/live/source.txt',
|
||
content: sourceContent
|
||
})
|
||
})
|
||
.catch(error => {
|
||
showMessageModal('保存失败: ' + error);
|
||
});
|
||
}
|
||
|
||
document.getElementById('sourceUrlTextarea').addEventListener('blur', saveLiveSourceFile);
|
||
|
||
// 显示更多直播源设置
|
||
function showMoreLiveSetting(sourceSync, nameProcess) {
|
||
showMessageModal('');
|
||
document.getElementById('messageModalMessage').innerHTML = `
|
||
<div class="button-container" style="width: 500px; margin-top: 30px;">
|
||
<button id="toggleLiveSourceSyncBtn" onclick="toggleStatus('toggleLiveSourceSyncBtn')">${sourceSync}</button>
|
||
<button id="toggleLiveChannelNameProcessBtn" onclick="toggleStatus('toggleLiveChannelNameProcessBtn')">${nameProcess}</button>
|
||
<button id="checkSourceBtn" onclick="checkSource()">测速校验</button>
|
||
<button id="cleanUnusedSourceBtn" onclick="cleanUnusedSource()">清理</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 保存编辑后的直播源信息
|
||
function saveLiveSourceInfo(popup = true, filePath = '') {
|
||
// 获取 checkbox 配置
|
||
const liveTvgLogoEnable = document.getElementById('live_tvg_logo_enable').value;
|
||
const liveTvgIdEnable = document.getElementById('live_tvg_id_enable').value;
|
||
const liveTvgNameEnable = document.getElementById('live_tvg_name_enable').value;
|
||
|
||
// 保存直播源信息
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({
|
||
save_source_info: 'true',
|
||
live_tvg_logo_enable: liveTvgLogoEnable,
|
||
live_tvg_id_enable: liveTvgIdEnable,
|
||
live_tvg_name_enable: liveTvgNameEnable,
|
||
file_path: filePath,
|
||
content: JSON.stringify(allLiveData)
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (popup) {
|
||
showMessageModal(data.success ? '保存成功<br>已生成 M3U 及 TXT 文件' : '保存失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
if (popup) {
|
||
showMessageModal('保存过程中出现错误: ' + error);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 直播源信息另存为新文件
|
||
function saveLiveSourceInfoAs() {
|
||
showMessageModal('');
|
||
document.getElementById('messageModalMessage').innerHTML = `
|
||
<div style="width: 180px;">
|
||
<h3>另存为</h3>
|
||
<input type="text" value="" id="fileName" style="text-align: center; font-size: 15px; margin-bottom: 15px;" />
|
||
<button id="confirmBtn" style="margin-bottom: -10px;">确认</button>
|
||
</div>
|
||
`;
|
||
|
||
// 添加按钮点击事件,点击后另存为新文件
|
||
document.getElementById('confirmBtn').onclick = function () {
|
||
fileName = document.getElementById('fileName').value;
|
||
saveLiveSourceInfo(popup = false, fileName);
|
||
|
||
// 检查并添加 fileName 到文本框
|
||
let t = document.getElementById('sourceUrlTextarea');
|
||
if (!t.value.split('\n').some(line => line.replace(/[#\s]/g, '').trim() === fileName)) {
|
||
t.value += `\n# ${fileName}`;
|
||
t.scrollTop = t.scrollHeight;
|
||
saveLiveSourceFile();
|
||
}
|
||
|
||
const [token, serverUrl, tokenRange] = document.getElementById('showLiveUrlBtn')
|
||
.getAttribute('onclick')
|
||
.match(/\`(.*?)\`/g)
|
||
.map(s => s.slice(1, -1));
|
||
var tokenStr = (tokenRange == 1 || tokenRange == 3) ? `token=${token}&` : '';
|
||
var m3uUrl = `${serverUrl}/index.php?${tokenStr}live=m3u&url=${fileName}`;
|
||
var txtUrl = `${serverUrl}/index.php?${tokenStr}live=txt&url=${fileName}`;
|
||
message = `成功另存为 ${fileName}<br>
|
||
M3U:<br><a href="${m3uUrl}" target="_blank">${m3uUrl}</a><br>
|
||
TXT:<br><a href="${txtUrl}" target="_blank">${txtUrl}`;
|
||
showMessageModal(message);
|
||
};
|
||
}
|
||
|
||
// 检验每个直播源的访问速度及分辨率
|
||
function checkSource() {
|
||
showMessageModal('');
|
||
const messageContainer = document.getElementById('messageModalMessage');
|
||
|
||
// 设置说明和开始测试、清除结果按钮
|
||
messageContainer.innerHTML = `
|
||
<div>
|
||
即将开始检测每个直播源的访问速度及分辨率,<br>
|
||
该过程可能需要一些时间,请耐心等待。<br><br>
|
||
注意:结果不一定准确,且暂无法解析 IPv6 源。<br><br>
|
||
</div>
|
||
<div class="button-container">
|
||
<button id="confirmCheckBtn" style="margin-bottom: -10px;">开始测速</button>
|
||
<button id="cleanCheckResultBtn" style="margin-bottom: -10px;">清除结果</button>
|
||
</div>
|
||
`;
|
||
|
||
// 开始测速
|
||
document.getElementById('confirmCheckBtn').onclick = function () {
|
||
showExecResult('check.php', () => showModal('live', popup = false));
|
||
};
|
||
|
||
// 清除结果
|
||
document.getElementById('cleanCheckResultBtn').onclick = function () {
|
||
showExecResult('check.php?cleanMode=true', () => showModal('live', popup = false), fullSize = false);
|
||
};
|
||
}
|
||
|
||
// 清理未使用的直播源文件
|
||
function cleanUnusedSource() {
|
||
fetch('manage.php?delete_unused_live_data=true')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessageModal(data.message);
|
||
} else {
|
||
showMessageModal('清理失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessageModal('Error: ' + error);
|
||
});
|
||
}
|
||
|
||
// 显示直播源地址
|
||
function showLiveUrl(token, serverUrl, tokenRange) {
|
||
var tokenStr = (tokenRange == 1 || tokenRange == 3) ? `token=${token}&` : '';
|
||
var m3uUrl = `${serverUrl}/index.php?${tokenStr}live=m3u`;
|
||
var txtUrl = `${serverUrl}/index.php?${tokenStr}live=txt`;
|
||
message = `M3U:<br><a href="${m3uUrl}" target="_blank">${m3uUrl}</a>
|
||
 <a href="${m3uUrl}" download="tv.m3u">下载</a><br>
|
||
TXT:<br><a href="${txtUrl}" target="_blank">${txtUrl}</a>
|
||
  <a href="${txtUrl}" download="tv.txt">下载</a><br>
|
||
转换:<br>${m3uUrl}&url=xxx<br>${txtUrl}&url=xxx`;
|
||
showMessageModal(message);
|
||
}
|
||
|
||
// 显示直播源模板
|
||
function showLiveTemplate() {
|
||
showModalWithMessage("liveTemplateModal");
|
||
}
|
||
|
||
// 保存编辑后的直播源模板
|
||
function saveLiveTemplate() {
|
||
// 保存配置
|
||
liveTemplateEnable = document.getElementById('live_template_enable').value;
|
||
liveFuzzyMatch = document.getElementById('live_fuzzy_match').value;
|
||
liveUrlComment = document.getElementById('live_url_comment').value;
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({
|
||
update_config_field: 'true',
|
||
live_template_enable: liveTemplateEnable,
|
||
live_fuzzy_match: liveFuzzyMatch,
|
||
live_url_comment: liveUrlComment
|
||
})
|
||
});
|
||
|
||
// 内容写入 template.txt 文件
|
||
liveTemplateContent = document.getElementById('liveTemplateTextarea').value;
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({
|
||
save_content_to_file: 'true',
|
||
file_path: '/data/live/template.txt',
|
||
content: liveTemplateContent
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
parseSourceInfo("保存成功<br>正在重新解析...");
|
||
document.getElementById('liveTemplateModal').style.display = 'none';
|
||
} else {
|
||
showMessageModal('保存失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessageModal('保存失败: ' + error);
|
||
});
|
||
}
|
||
|
||
// 搜索频道
|
||
function filterChannels(type) {
|
||
const tableId = type === 'channel' ? 'channelTable' : 'iconTable';
|
||
const dataAttr = type === 'channel' ? 'allChannels' : 'allIcons';
|
||
const input = document.getElementById(type === 'channel' ? 'channelSearchInput' : 'iconSearchInput').value.toUpperCase();
|
||
const tableBody = document.querySelector(`#${tableId} tbody`);
|
||
const allData = JSON.parse(document.getElementById(tableId).dataset[dataAttr]);
|
||
|
||
tableBody.innerHTML = ''; // 清空表格
|
||
|
||
// 创建行的通用函数
|
||
function createEditableRow(item, itemIndex, insertAfterRow = null) {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td name="channel" contenteditable="true" onclick="this.innerText='';"><span style="color: #aaa;">创建自定义频道</span>${item.channel || ''}</td>
|
||
<td name="icon" contenteditable="true">${item.icon || ''}</td>
|
||
<td></td>
|
||
<td>
|
||
<input type="file" accept="image/png" style="display:none;" id="icon_new_${itemIndex}">
|
||
<button onclick="document.getElementById('icon_new_${itemIndex}').click()" style="font-size: 14px; width: 50px;">上传</button>
|
||
</td>
|
||
`;
|
||
|
||
// 动态更新 allData
|
||
row.querySelectorAll('td[contenteditable]').forEach(cell => {
|
||
cell.addEventListener('input', () => {
|
||
allData[itemIndex][cell.getAttribute('name')] = cell.textContent.trim();
|
||
document.getElementById(tableId).dataset[dataAttr] = JSON.stringify(allData);
|
||
if (cell.getAttribute('name') === 'channel' && item.channel && !allData.some(e => !e.channel)) {
|
||
allData.push({ channel: '', icon: '' });
|
||
createEditableRow(allData[allData.length - 1], allData.length - 1, row); // 插入新行到当前行后
|
||
}
|
||
});
|
||
});
|
||
|
||
// 上传文件
|
||
row.querySelector(`#icon_new_${itemIndex}`).addEventListener('change', event => handleIconFileUpload(event, item, row, allData));
|
||
|
||
// 如果指定了插入位置,则插入到该行之后,否则追加到表格末尾
|
||
if (insertAfterRow) {
|
||
insertAfterRow.insertAdjacentElement('afterend', row);
|
||
} else {
|
||
tableBody.appendChild(row);
|
||
}
|
||
}
|
||
|
||
// 创建初始空行(仅用于 icon)
|
||
if (!input && type === 'icon') {
|
||
allData.push({ channel: '', icon: '' });
|
||
createEditableRow(allData[allData.length - 1], allData.length - 1);
|
||
}
|
||
|
||
// 筛选并显示行的逻辑
|
||
allData.forEach((item, index) => {
|
||
const searchText = type === 'channel' ? item.original : item.channel;
|
||
if (String(searchText).toUpperCase().includes(input)) {
|
||
const row = document.createElement('tr');
|
||
if (type === 'channel') {
|
||
row.innerHTML = `<td class="blue-span"
|
||
onclick="showModal('epg', true, { channel: '${item.original}', date: '${new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Shanghai' })}' })">
|
||
${item.original} </td>
|
||
<td contenteditable="true">${item.mapped || ''}</td>`;
|
||
row.querySelector('td[contenteditable]').addEventListener('input', function () {
|
||
item.mapped = this.textContent.trim();
|
||
document.getElementById(tableId).dataset[dataAttr] = JSON.stringify(allData);
|
||
});
|
||
} else if (type === 'icon' && searchText) {
|
||
row.innerHTML = `
|
||
<td contenteditable="true">${item.channel}</td>
|
||
<td contenteditable="true">${item.icon || ''}</td>
|
||
<td>${item.icon ? `<a href="${item.icon}" target="_blank"><img loading="lazy" src="${item.icon}" style="max-width: 80px; max-height: 50px; background-color: #ccc;"></a>` : ''}</td>
|
||
<td>
|
||
<input type="file" accept="image/png" style="display:none;" id="file_${index}">
|
||
<button onclick="document.getElementById('file_${index}').click()" style="font-size: 14px; width: 50px;">上传</button>
|
||
</td>
|
||
`;
|
||
row.querySelectorAll('td[contenteditable]').forEach((cell, idx) => {
|
||
cell.addEventListener('input', function () {
|
||
if (idx === 0) item.channel = this.textContent.trim(); // 第一个可编辑单元格更新 channel
|
||
else item.icon = this.textContent.trim(); // 第二个可编辑单元格更新 icon
|
||
document.getElementById(tableId).dataset[dataAttr] = JSON.stringify(allData);
|
||
});
|
||
});
|
||
row.querySelector(`#file_${index}`).addEventListener('change', event => handleIconFileUpload(event, item, row, allData));
|
||
}
|
||
tableBody.appendChild(row);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 台标上传
|
||
function handleIconFileUpload(event, item, row, allData) {
|
||
const file = event.target.files[0];
|
||
if (file && file.type === 'image/png') {
|
||
const formData = new FormData();
|
||
formData.append('iconFile', file);
|
||
|
||
fetch('manage.php', { method: 'POST', body: formData })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const iconUrl = data.iconUrl;
|
||
row.cells[1].innerText = iconUrl;
|
||
item.icon = iconUrl;
|
||
row.cells[2].innerHTML = `
|
||
<a href="${iconUrl}?${new Date().getTime()}" target="_blank">
|
||
<img loading="lazy" src="${iconUrl}?${new Date().getTime()}" style="max-width: 80px; max-height: 50px; background-color: #ccc;">
|
||
</a>
|
||
`;
|
||
document.getElementById('iconTable').dataset.allIcons = JSON.stringify(allData);
|
||
updateIconListJsonFile();
|
||
} else {
|
||
showMessageModal('上传失败:' + data.message);
|
||
}
|
||
})
|
||
.catch(error => showMessageModal('上传过程中发生错误:' + error));
|
||
} else {
|
||
showMessageModal('请选择PNG文件上传');
|
||
}
|
||
event.target.value = ''; // 重置文件输入框的值,确保可以连续上传相同文件
|
||
}
|
||
|
||
// 转存所有台标到服务器
|
||
function uploadAllIcons() {
|
||
const serverUrl = window.location.origin;
|
||
const iconTable = document.getElementById('iconTable');
|
||
const allIcons = JSON.parse(iconTable.dataset.allIcons);
|
||
const rows = Array.from(document.querySelectorAll('#iconTable tbody tr'));
|
||
|
||
let totalIcons = 0;
|
||
let uploadedIcons = 0;
|
||
const rowsToUpload = rows.filter(row => {
|
||
const iconUrl = row.cells[1]?.innerText.trim();
|
||
if (iconUrl) {
|
||
totalIcons++;
|
||
if (!iconUrl.startsWith(serverUrl)) {
|
||
return true;
|
||
} else {
|
||
uploadedIcons++;
|
||
}
|
||
}
|
||
return false;
|
||
});
|
||
|
||
const progressDisplay = document.getElementById('progressDisplay') || document.createElement('div');
|
||
progressDisplay.id = 'progressDisplay';
|
||
progressDisplay.style.cssText = 'margin: 10px 0; text-align: right;';
|
||
progressDisplay.textContent = `已转存 ${uploadedIcons}/${totalIcons}`;
|
||
iconTable.before(progressDisplay);
|
||
|
||
const uploadPromises = rowsToUpload.map(row => {
|
||
const [channelCell, iconCell, previewCell] = row.cells;
|
||
const iconUrl = iconCell?.innerText.trim();
|
||
const fileName = decodeURIComponent(iconUrl.split('/').pop().split('?')[0]);
|
||
|
||
return fetch(iconUrl)
|
||
.then(res => res.blob())
|
||
.then(blob => {
|
||
const formData = new FormData();
|
||
formData.append('iconFile', new File([blob], fileName, { type: 'image/png' }));
|
||
|
||
return fetch('manage.php', { method: 'POST', body: formData });
|
||
})
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const iconUrl = data.iconUrl;
|
||
const channelName = channelCell.innerText.trim();
|
||
iconCell.innerText = iconUrl;
|
||
previewCell.innerHTML = `
|
||
<a href="${iconUrl}?${Date.now()}" target="_blank">
|
||
<img loading="lazy" src="${iconUrl}?${Date.now()}" style="max-width: 80px; max-height: 50px; background-color: #ccc;">
|
||
</a>
|
||
`;
|
||
|
||
allIcons.forEach(item => {
|
||
if (item.channel === channelName) item.icon = iconUrl;
|
||
});
|
||
iconTable.dataset.allIcons = JSON.stringify(allIcons);
|
||
uploadedIcons++;
|
||
progressDisplay.textContent = `已转存 ${uploadedIcons}/${totalIcons}`;
|
||
} else {
|
||
previewCell.innerHTML = `上传失败: ${data.message}`;
|
||
}
|
||
})
|
||
.catch(() => {
|
||
previewCell.innerHTML = '上传出错';
|
||
});
|
||
});
|
||
|
||
Promise.all(uploadPromises).then(() => {
|
||
if (uploadedIcons !== totalIcons) {
|
||
uploadAllIcons(); // 继续上传
|
||
}
|
||
else {
|
||
updateIconListJsonFile();
|
||
showMessageModal("全部转存成功,已保存!");
|
||
}
|
||
});
|
||
}
|
||
|
||
// 清理未使用的台标文件
|
||
function deleteUnusedIcons() {
|
||
fetch('manage.php?delete_unused_icons=true')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessageModal(data.message);
|
||
} else {
|
||
showMessageModal('清理失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessageModal('Error: ' + error);
|
||
});
|
||
}
|
||
|
||
// 更新频道别名
|
||
function updateChannelMapping() {
|
||
var allChannels = JSON.parse(document.getElementById('channelTable').dataset.allChannels);
|
||
var existingMappings = document.getElementById('channel_mappings').value.split('\n');
|
||
|
||
// 过滤出现有映射中的正则表达式映射
|
||
var regexMappings = existingMappings.filter(line => line.includes('regex:'));
|
||
|
||
// 生成新的频道别名映射
|
||
var newMappings = allChannels
|
||
.filter(channel => channel.mapped.trim() !== '')
|
||
.map(channel => `${channel.original} => ${channel.mapped}`);
|
||
|
||
// 更新映射文本框并保存配置
|
||
document.getElementById('channel_mappings').value = [...newMappings, ...regexMappings].join('\n');
|
||
updateConfig();
|
||
}
|
||
|
||
// 解析 txt、m3u 直播源,并生成频道列表(仅频道)
|
||
async function parseSource() {
|
||
const textarea = document.getElementById('gen_list_text');
|
||
let text = textarea.value.trim();
|
||
const channels = new Set();
|
||
|
||
// 拆分输入的内容,可能包含多个 URL 或文本
|
||
if (!text.includes('#EXTM3U')) {
|
||
let lines = text.split('\n').map(line => line.trim());
|
||
let urls = lines.filter(line => line.startsWith('http'));
|
||
|
||
// 如果存在 URL,则清空原本的 text 内容并逐个请求获取数据
|
||
if (urls.length > 0) {
|
||
text = '';
|
||
for (let url of urls) {
|
||
try {
|
||
const response = await fetch('manage.php?download_data=true&url=' + encodeURIComponent(url));
|
||
const result = await response.json(); // 解析 JSON 响应
|
||
|
||
if (result.success && !/not found/i.test(result.data)) {
|
||
text += '\n' + result.data;
|
||
} else {
|
||
showMessageModal(/not found/i.test(result.data) ? `Error: ${result.data}` : `${result.message}:\n${url}`);
|
||
}
|
||
} catch (error) {
|
||
showMessageModal(`无法获取URL内容: ${url}\n错误信息: ${error.message}`); // 显示网络错误信息
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理 m3u 、 txt 文件内容
|
||
text.split('\n').forEach(line => {
|
||
if (line && !/^http/i.test(line) && !/#genre#/i.test(line) && !/#extm3u/i.test(line)) {
|
||
if (/^#extinf:/i.test(line)) {
|
||
const tvgIdMatch = line.match(/tvg-id="([^"]+)"/i);
|
||
const tvgNameMatch = line.match(/tvg-name="([^"]+)"/i);
|
||
|
||
channelName = (tvgIdMatch && /\D/.test(tvgIdMatch[1]) ? tvgIdMatch[1] : tvgNameMatch ? tvgNameMatch[1] : line.split(',').slice(-1)[0]).trim();
|
||
} else {
|
||
channelName = line.split(',')[0].trim();
|
||
}
|
||
if (channelName) channels.add(channelName.toUpperCase());
|
||
}
|
||
});
|
||
|
||
// 将解析后的频道列表放回文本区域
|
||
textarea.value = Array.from(channels).join('\n');
|
||
|
||
// 保存限定频道列表到数据库
|
||
setGenList();
|
||
}
|
||
|
||
// 解析 txt、m3u 直播源,并生成直播列表(包含分组、地址等信息)
|
||
function parseSourceInfo(message = '') {
|
||
showMessageModal(message || "在线源解析较慢<br>请耐心等待...");
|
||
|
||
fetch(`manage.php?parse_source_info=true`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showModal('live');
|
||
if (data.success == 'full') {
|
||
showMessageModal('解析成功<br>已生成 M3U 及 TXT 文件');
|
||
} else if (data.success == 'part') {
|
||
showMessageModal('已生成 M3U 及 TXT 文件<br>部分源异常<br>' + data.message);
|
||
}
|
||
})
|
||
.catch(error => showMessageModal('解析过程中发生错误:' + error));
|
||
}
|
||
|
||
// 保存限定频道列表
|
||
async function setGenList() {
|
||
const genListText = document.getElementById('gen_list_text').value;
|
||
try {
|
||
const response = await fetch('manage.php?set_gen_list=true', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ data: genListText })
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
|
||
if (responseText.trim() !== 'success') {
|
||
console.error('服务器响应错误:', responseText);
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// 保存限定频道列表并更新配置
|
||
function setGenListAndUpdateConfig() {
|
||
setGenList();
|
||
updateConfig();
|
||
}
|
||
|
||
// 更新台标文件 iconList.json
|
||
function updateIconListJsonFile(notify = false) {
|
||
var iconTableElement = document.getElementById('iconTable');
|
||
var allIcons = iconTableElement && iconTableElement.dataset.allIcons ? JSON.parse(iconTableElement.dataset.allIcons) : null;
|
||
if (allIcons) {
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: new URLSearchParams({
|
||
update_icon_list: true,
|
||
updatedIcons: JSON.stringify(allIcons) // 传递更新后的图标数据
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && notify) {
|
||
showModal('icon');
|
||
showMessageModal('保存成功');
|
||
} else if (data.success == false) {
|
||
showMessageModal(data.message);
|
||
}
|
||
})
|
||
.catch(error => showMessageModal('更新过程中发生错误:' + error));
|
||
}
|
||
}
|
||
|
||
// 导入配置
|
||
document.getElementById('importFile').addEventListener('change', function () {
|
||
const file = this.files[0];
|
||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||
|
||
// 检查文件类型
|
||
if (fileExtension != 'gz') {
|
||
showMessageModal('只接受 .gz 文件');
|
||
return;
|
||
}
|
||
|
||
// 发送 AJAX 请求
|
||
const formData = new FormData(document.getElementById('importForm'));
|
||
|
||
fetch('manage.php', { method: 'POST', body: formData })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessageModal(data.message);
|
||
if (data.success) {
|
||
// 延迟刷新页面
|
||
setTimeout(() => {
|
||
window.location.href = 'manage.php';
|
||
}, 3000);
|
||
}
|
||
})
|
||
.catch(error => showMessageModal('导入过程中发生错误:' + error));
|
||
|
||
this.value = ''; // 重置文件输入框的值,确保可以连续上传相同文件
|
||
});
|
||
|
||
// 修改 token、user_agent 对话框
|
||
function changeTokenUA(type, currentTokenUA) {
|
||
showMessageModal('');
|
||
typeStr = (type === 'token' ? 'Token' : 'User-Agent') + '<br>支持多个,逗号分隔';
|
||
document.getElementById('messageModalMessage').innerHTML = `
|
||
<div style="width: 180px;">
|
||
<h3>修改 ${typeStr}</h3>
|
||
<input type="text" value="${currentTokenUA}" id="newTokenUA" style="text-align: center; font-size: 15px; margin-bottom: 15px;" />
|
||
<button onclick="updateTokenUA('${type}')" style="margin-bottom: -10px;">确认</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 更新 token、user_agent 到 config.json
|
||
function updateTokenUA(type) {
|
||
var newTokenUA = document.getElementById('newTokenUA').value.replace(/,/g, ","); // 将中文逗号替换为英文逗号
|
||
|
||
// 内容写入 config.json 文件
|
||
fetch('manage.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: new URLSearchParams({
|
||
update_config_field: 'true',
|
||
[type.toLowerCase()]: newTokenUA
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
if (type.toLowerCase() == 'token' || newTokenUA == '') {
|
||
alert('修改成功');
|
||
window.location.href = 'manage.php';
|
||
}
|
||
else {
|
||
showMessageModal('修改成功');
|
||
document.getElementById('change_ua_span').setAttribute('onclick', `changeTokenUA('user_agent', '${newTokenUA}')`);
|
||
}
|
||
} else {
|
||
showMessageModal('修改失败');
|
||
}
|
||
})
|
||
.catch(error => showMessageModal('保存过程中出现错误: ' + error));
|
||
}
|
||
|
||
// token_range 更变后进行提示
|
||
function showTokenRangeMessage(token, serverUrl) {
|
||
var tokenRange = document.getElementById("token_range").value;
|
||
var message = '';
|
||
var baseUrl = serverUrl + '/index.php?token=' + token;
|
||
if (tokenRange == "1" || tokenRange == "3") {
|
||
message += `直播源地址:<br><a href="${baseUrl}&live=m3u" target="_blank">${baseUrl}&live=m3u</a><br>
|
||
<a href="${baseUrl}&live=txt" target="_blank">${baseUrl}&live=txt</a>`;
|
||
}
|
||
if (tokenRange == "2" || tokenRange == "3") {
|
||
if (message) message += '<br>';
|
||
message += `EPG地址:<br><a href="${baseUrl}" target="_blank">${baseUrl}</a>`;
|
||
}
|
||
if (message) {
|
||
showMessageModal(message);
|
||
}
|
||
document.getElementById('showLiveUrlBtn').setAttribute('onclick', `showLiveUrl('${token}', '${serverUrl}', '${tokenRange}')`);
|
||
}
|
||
|
||
// 切换主题
|
||
document.getElementById('themeSwitcher').addEventListener('click', function () {
|
||
// 获取当前主题,并切换到下一个主题
|
||
const currentTheme = localStorage.getItem('theme');
|
||
const newTheme = currentTheme === 'light' ? 'dark' : (currentTheme === 'dark' ? '' : 'light');
|
||
|
||
// 更新主题
|
||
document.body.classList.add('theme-transition');
|
||
document.body.classList.remove('dark', 'light');
|
||
|
||
if (newTheme === '') {
|
||
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||
document.body.classList.add(prefersDarkScheme ? 'dark' : 'light');
|
||
} else {
|
||
document.body.classList.add(newTheme);
|
||
}
|
||
|
||
// 更新图标和文字
|
||
document.getElementById("themeIcon");
|
||
const labelText = document.querySelector('.label-text');
|
||
themeIcon.className = `fas ${newTheme === 'dark' ? 'fa-moon' : newTheme === 'light' ? 'fa-sun' : 'fa-adjust'}`;
|
||
labelText.textContent = newTheme === 'dark' ? 'Dark' : newTheme === 'light' ? 'Light' : 'Auto';
|
||
|
||
// 保存到本地存储
|
||
localStorage.setItem('theme', newTheme);
|
||
});
|
||
|
||
// 监听系统主题变化
|
||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||
if (!localStorage.getItem('theme')) {
|
||
const theme = e.matches ? 'dark' : 'light';
|
||
document.body.classList.remove('dark', 'light');
|
||
document.body.classList.add(theme);
|
||
}
|
||
});
|