mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 14:24:14 +02:00
refactor(qml): extract header, search, and theme from Main.qml
Split the duplicated desktop/compact header UI into reusable QML components (HeaderNavigation, SearchToolbar, QuickFilters), centralize colors and world tags in BearTheme singleton, and move flag emoji logic to FlagUtils.js. Register modules via qrc import path qrc:/qml. Main.qml shrinks from ~1816 to ~1240 lines; behavior unchanged.
This commit is contained in:
@@ -49,6 +49,7 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.addImportPath(QStringLiteral("qrc:/qml"));
|
||||
|
||||
RadioBackend backend;
|
||||
engine.rootContext()->setContextProperty("radioBackend", &backend);
|
||||
@@ -70,7 +71,7 @@ int main(int argc, char *argv[])
|
||||
mprisPlayer->publishState();
|
||||
}
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/Main.qml"));
|
||||
const QUrl url(QStringLiteral("qrc:/qml/Main.qml"));
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||
&app, [&app, url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl) {
|
||||
|
||||
13
src/qml.qrc
13
src/qml.qrc
@@ -1,10 +1,19 @@
|
||||
<RCC>
|
||||
<qresource prefix="/qml">
|
||||
<file>qml/Main.qml</file>
|
||||
<file>qml/theme/qmldir</file>
|
||||
<file>qml/theme/BearTheme.qml</file>
|
||||
<file>qml/components/qmldir</file>
|
||||
<file>qml/components/HeaderNavigation.qml</file>
|
||||
<file>qml/components/SearchToolbar.qml</file>
|
||||
<file>qml/components/QuickFilters.qml</file>
|
||||
<file>qml/utils/FlagUtils.js</file>
|
||||
</qresource>
|
||||
<qresource prefix="/">
|
||||
<file alias="Main.qml">qml/Main.qml</file>
|
||||
<file alias="assets/app/bearwave.png">../assets/app/bearwave.png</file>
|
||||
<file alias="assets/app/bearwave_line.png">../assets/app/bearwave_line.png</file>
|
||||
<file alias="assets/ui/globe.svg">../assets/ui/globe.svg</file>
|
||||
<file alias="assets/ui/github.svg">../assets/ui/github.svg</file>
|
||||
<file alias="assets/ui/linkedin.svg">../assets/ui/linkedin.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
</RCC>
|
||||
729
src/qml/Main.qml
729
src/qml/Main.qml
File diff suppressed because it is too large
Load Diff
205
src/qml/components/HeaderNavigation.qml
Normal file
205
src/qml/components/HeaderNavigation.qml
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import theme 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var app
|
||||
required property bool compactMode
|
||||
|
||||
implicitWidth: compactMode ? compactNav.implicitWidth : desktopNav.implicitWidth
|
||||
implicitHeight: compactMode ? compactNav.implicitHeight : desktopNav.implicitHeight
|
||||
|
||||
function loadTop() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "top"
|
||||
app.activeQuickFilter = ""
|
||||
app.backend.loadTopStations()
|
||||
}
|
||||
|
||||
function loadGerman() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "german"
|
||||
app.activeQuickFilter = ""
|
||||
app.backend.loadGermanStations()
|
||||
}
|
||||
|
||||
function loadDutch() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "dutch"
|
||||
app.activeQuickFilter = ""
|
||||
app.backend.loadDutchStations()
|
||||
}
|
||||
|
||||
function showFavorites() {
|
||||
app.currentPage = "favorites"
|
||||
app.activeQuickFilter = ""
|
||||
}
|
||||
|
||||
function showHistory() {
|
||||
app.currentPage = "history"
|
||||
app.activeQuickFilter = ""
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: desktopNav
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: !root.compactMode
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 112
|
||||
Layout.preferredHeight: 40
|
||||
source: "qrc:/assets/app/bearwave_line.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 1
|
||||
Layout.fillHeight: true
|
||||
color: BearTheme.cardBorder
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Top")
|
||||
highlighted: app.currentPage === "top"
|
||||
onClicked: loadTop()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("DE")
|
||||
highlighted: app.currentPage === "german"
|
||||
onClicked: loadGerman()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("NL")
|
||||
highlighted: app.currentPage === "dutch"
|
||||
onClicked: loadDutch()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Favorites")
|
||||
highlighted: app.currentPage === "favorites"
|
||||
onClicked: showFavorites()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("History")
|
||||
highlighted: app.currentPage === "history"
|
||||
onClicked: showHistory()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: qsTr("Manual +")
|
||||
onClicked: app.addDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("About")
|
||||
onClicked: app.aboutDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: app.backend && app.backend.canResumeLastStation
|
||||
text: qsTr("Resume")
|
||||
onClicked: {
|
||||
if (app.backend) {
|
||||
app.backend.resumeLastStation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: compactNav
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
visible: root.compactMode
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 96
|
||||
Layout.preferredHeight: 34
|
||||
source: "qrc:/assets/app/bearwave_line.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: "+"
|
||||
onClicked: app.addDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("About")
|
||||
onClicked: app.aboutDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: app.backend && app.backend.canResumeLastStation
|
||||
text: "↺"
|
||||
onClicked: {
|
||||
if (app.backend) {
|
||||
app.backend.resumeLastStation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
text: qsTr("Top")
|
||||
highlighted: app.currentPage === "top"
|
||||
onClicked: loadTop()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("DE")
|
||||
highlighted: app.currentPage === "german"
|
||||
onClicked: loadGerman()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("NL")
|
||||
highlighted: app.currentPage === "dutch"
|
||||
onClicked: loadDutch()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Favorites")
|
||||
highlighted: app.currentPage === "favorites"
|
||||
onClicked: showFavorites()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("History")
|
||||
highlighted: app.currentPage === "history"
|
||||
onClicked: showHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
src/qml/components/QuickFilters.qml
Normal file
190
src/qml/components/QuickFilters.qml
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import theme 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var app
|
||||
required property bool compactMode
|
||||
|
||||
spacing: 8
|
||||
|
||||
function loadTag(tag, page) {
|
||||
if (!app.backend) return
|
||||
app.currentPage = page
|
||||
app.activeQuickFilter = "tag:" + tag
|
||||
app.backend.loadByTag(tag)
|
||||
}
|
||||
|
||||
function loadCountry(code, page) {
|
||||
if (!app.backend) return
|
||||
app.currentPage = page
|
||||
app.activeQuickFilter = "cc:" + code
|
||||
app.backend.loadByCountryCode(code)
|
||||
}
|
||||
|
||||
function openWorld() {
|
||||
if (!app.backend) return
|
||||
app.currentPage = "world"
|
||||
app.activeQuickFilter = "world"
|
||||
app.selectedWorldCategory = ""
|
||||
app.selectedWorldType = ""
|
||||
if (app.backend.countries.length === 0) {
|
||||
app.backend.loadCountries()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: !root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
text: qsTr("Genre:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
rightPadding: 10
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Rock")
|
||||
highlighted: app.activeQuickFilter === "tag:rock"
|
||||
onClicked: loadTag("rock", "genre-rock")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("News")
|
||||
highlighted: app.activeQuickFilter === "tag:news"
|
||||
onClicked: loadTag("news", "genre-news")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Jazz")
|
||||
highlighted: app.activeQuickFilter === "tag:jazz"
|
||||
onClicked: loadTag("jazz", "genre-jazz")
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 20 }
|
||||
|
||||
Label {
|
||||
text: qsTr("Country:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
rightPadding: 10
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("US")
|
||||
highlighted: app.activeQuickFilter === "cc:US"
|
||||
onClicked: loadCountry("US", "country-us")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("UK")
|
||||
highlighted: app.activeQuickFilter === "cc:GB"
|
||||
onClicked: loadCountry("GB", "country-gb")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("FR")
|
||||
highlighted: app.activeQuickFilter === "cc:FR"
|
||||
onClicked: loadCountry("FR", "country-fr")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("WORLD")
|
||||
highlighted: app.activeQuickFilter === "world"
|
||||
onClicked: openWorld()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: qsTr("Genre:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 8
|
||||
rightPadding: 16
|
||||
topPadding: 8
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Rock")
|
||||
highlighted: app.activeQuickFilter === "tag:rock"
|
||||
onClicked: loadTag("rock", "genre-rock")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("News")
|
||||
highlighted: app.activeQuickFilter === "tag:news"
|
||||
onClicked: loadTag("news", "genre-news")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Jazz")
|
||||
highlighted: app.activeQuickFilter === "tag:jazz"
|
||||
onClicked: loadTag("jazz", "genre-jazz")
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: qsTr("Country:")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 8
|
||||
rightPadding: 16
|
||||
topPadding: 8
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("US")
|
||||
highlighted: app.activeQuickFilter === "cc:US"
|
||||
onClicked: loadCountry("US", "country-us")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("UK")
|
||||
highlighted: app.activeQuickFilter === "cc:GB"
|
||||
onClicked: loadCountry("GB", "country-gb")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("FR")
|
||||
highlighted: app.activeQuickFilter === "cc:FR"
|
||||
onClicked: loadCountry("FR", "country-fr")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("WORLD")
|
||||
highlighted: app.activeQuickFilter === "world"
|
||||
onClicked: openWorld()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/qml/components/SearchToolbar.qml
Normal file
166
src/qml/components/SearchToolbar.qml
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import theme 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var app
|
||||
required property var searchTimer
|
||||
required property bool compactMode
|
||||
|
||||
property alias searchField: searchField
|
||||
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
visible: !root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Search stations (name, genre, country)")
|
||||
onTextChanged: {
|
||||
if (app.backend) {
|
||||
app.backend.filterQuery = text
|
||||
}
|
||||
searchTimer.restart()
|
||||
}
|
||||
onAccepted: {
|
||||
if (text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(text)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Search")
|
||||
highlighted: true
|
||||
onClicked: {
|
||||
if (searchField.text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(searchField.text)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Sort A-Z")
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Sort Bitrate")
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("bitrate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Sort Votes")
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("votes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.compactMode
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
TextField {
|
||||
id: compactSearchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Search stations (name, genre, country)")
|
||||
text: searchField.text
|
||||
onTextChanged: {
|
||||
if (searchField.text !== text) {
|
||||
searchField.text = text
|
||||
}
|
||||
if (app.backend) {
|
||||
app.backend.filterQuery = text
|
||||
}
|
||||
searchTimer.restart()
|
||||
}
|
||||
onAccepted: {
|
||||
if (text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(text)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: qsTr("Search")
|
||||
highlighted: true
|
||||
onClicked: {
|
||||
if (compactSearchField.text.length < 2 || !app.backend) return
|
||||
app.currentPage = "search"
|
||||
app.backend.searchStations(compactSearchField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
text: "A-Z"
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "kb"
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("bitrate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "❤"
|
||||
onClicked: {
|
||||
if (app.backend && app.currentPage !== "favorites") {
|
||||
app.backend.sortStations("votes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Tip: you are not limited to DE/NL. Search worldwide by country, genre, or station name.")
|
||||
color: BearTheme.textMuted
|
||||
font.pixelSize: 11
|
||||
wrapMode: root.compactMode ? Text.WordWrap : Text.NoWrap
|
||||
elide: root.compactMode ? Text.ElideNone : Text.ElideRight
|
||||
}
|
||||
}
|
||||
3
src/qml/components/qmldir
Normal file
3
src/qml/components/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
HeaderNavigation 1.0 HeaderNavigation.qml
|
||||
SearchToolbar 1.0 SearchToolbar.qml
|
||||
QuickFilters 1.0 QuickFilters.qml
|
||||
33
src/qml/theme/BearTheme.qml
Normal file
33
src/qml/theme/BearTheme.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma Singleton
|
||||
import QtQuick 2.15
|
||||
|
||||
QtObject {
|
||||
readonly property color bgA: "#0f141b"
|
||||
readonly property color bgB: "#131b25"
|
||||
readonly property color panel: "#182433"
|
||||
readonly property color card: "#1b2a3d"
|
||||
readonly property color cardHover: "#223654"
|
||||
readonly property color cardBorder: "#2d4566"
|
||||
readonly property color accent: "#2bb0ff"
|
||||
readonly property color textMain: "#eaf1fb"
|
||||
readonly property color textMuted: "#9eb1c9"
|
||||
readonly property color warn: "#ff8b8b"
|
||||
|
||||
readonly property var worldTags: [
|
||||
{ name: qsTr("Pop"), tag: "pop", icon: "🎵" },
|
||||
{ name: qsTr("Rock"), tag: "rock", icon: "🎸" },
|
||||
{ name: qsTr("Electronic"), tag: "electronic", icon: "🎹" },
|
||||
{ name: qsTr("Classical"), tag: "classical", icon: "🎻" },
|
||||
{ name: qsTr("Jazz"), tag: "jazz", icon: "🎷" },
|
||||
{ name: qsTr("Metal"), tag: "metal", icon: "🤘" },
|
||||
{ name: qsTr("Hip Hop"), tag: "hiphop", icon: "🎤" },
|
||||
{ name: qsTr("Chillout"), tag: "chillout", icon: "🌴" },
|
||||
{ name: qsTr("News / Talk"), tag: "news", icon: "📻" },
|
||||
{ name: qsTr("Soundtracks"), tag: "soundtrack", icon: "🎬" },
|
||||
{ name: qsTr("Ambient"), tag: "ambient", icon: "🌌" },
|
||||
{ name: qsTr("Blues / Soul"), tag: "blues", icon: "🎺" }
|
||||
]
|
||||
}
|
||||
1
src/qml/theme/qmldir
Normal file
1
src/qml/theme/qmldir
Normal file
@@ -0,0 +1 @@
|
||||
singleton BearTheme 1.0 BearTheme.qml
|
||||
16
src/qml/utils/FlagUtils.js
Normal file
16
src/qml/utils/FlagUtils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2026 Sebastian Palencsar
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
.pragma library
|
||||
|
||||
function flagEmoji(countryCode) {
|
||||
if (!countryCode || countryCode.length !== 2) {
|
||||
return "🌎"
|
||||
}
|
||||
var codeUpper = countryCode.toUpperCase()
|
||||
var codePoints = []
|
||||
for (var i = 0; i < codeUpper.length; i++) {
|
||||
codePoints.push(127397 + codeUpper.charCodeAt(i))
|
||||
}
|
||||
return String.fromCodePoint.apply(null, codePoints)
|
||||
}
|
||||
Reference in New Issue
Block a user