mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-22 00:35:34 +00:00
commit
72f1842e44
@ -59,4 +59,11 @@
|
|||||||
#pywebio-scope-waiting,
|
#pywebio-scope-waiting,
|
||||||
#pywebio-scope-log {
|
#pywebio-scope-log {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[id^="pywebio-scope-dashboard-row-"] {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 4rem;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
@ -26,7 +26,16 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pywebio-scope-scheduler-bar,
|
#pywebio-scope-log-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 11.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pywebio-scope-dashboard {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#pywebio-scope-log-bar,
|
#pywebio-scope-log-bar,
|
||||||
#pywebio-scope-log,
|
#pywebio-scope-log,
|
||||||
#pywebio-scope-daemon-overview #pywebio-scope-groups {
|
#pywebio-scope-daemon-overview #pywebio-scope-groups {
|
||||||
@ -45,4 +54,4 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
}
|
}
|
||||||
|
@ -383,17 +383,68 @@ pre.rich-traceback-code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#pywebio-scope-scheduler-bar,
|
#pywebio-scope-scheduler-bar,
|
||||||
#pywebio-scope-log-bar {
|
#pywebio-scope-log-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pywebio-scope-log-bar-btns {
|
#pywebio-scope-log-title-btns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pywebio-scope-dashboard {
|
||||||
|
display: flex;
|
||||||
|
align-content: space-between;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pywebio-scope-dashboard > i {
|
||||||
|
flex-grow: 1;
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[id^="pywebio-scope-dashboard-row-"] {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-icon {
|
||||||
|
margin: .6rem .8rem 0 .6rem;
|
||||||
|
width: .5rem;
|
||||||
|
height: .5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[style*="--dashboard-value--"] {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[style*="--dashboard-time--"] {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
[id^="pywebio-scope-dashboard-row-"] p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[id^="pywebio-scope-dashboard-value-"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#pywebio-scope-log {
|
#pywebio-scope-log {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
@ -152,4 +152,8 @@ pre.rich-traceback-code {
|
|||||||
*[style*="--arg-help--"],
|
*[style*="--arg-help--"],
|
||||||
[id^="pywebio-scope-group_"] > p + p {
|
[id^="pywebio-scope-group_"] > p + p {
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[style*="--dashboard-time--"] {
|
||||||
|
color: #adb5bd;
|
||||||
}
|
}
|
@ -151,4 +151,8 @@ pre.rich-traceback-code {
|
|||||||
*[style*="--arg-help--"],
|
*[style*="--arg-help--"],
|
||||||
[id^="pywebio-scope-group_"] > p + p {
|
[id^="pywebio-scope-group_"] > p + p {
|
||||||
color: #777777;
|
color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[style*="--dashboard-time--"] {
|
||||||
|
color: #777777;
|
||||||
}
|
}
|
BIN
assets/share/dungeon/ui/OCR_SIMUNI_POINT.png
Normal file
BIN
assets/share/dungeon/ui/OCR_SIMUNI_POINT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
assets/support/selected_character_arrow.png
Normal file
BIN
assets/support/selected_character_arrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 668 B |
@ -57,7 +57,8 @@
|
|||||||
},
|
},
|
||||||
"DungeonStorage": {
|
"DungeonStorage": {
|
||||||
"TrailblazePower": {},
|
"TrailblazePower": {},
|
||||||
"DungeonDouble": {}
|
"DungeonDouble": {},
|
||||||
|
"SimulatedUniverse": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DailyQuest": {
|
"DailyQuest": {
|
||||||
@ -119,7 +120,8 @@
|
|||||||
"Name_2": "Akashic_Records",
|
"Name_2": "Akashic_Records",
|
||||||
"Name_3": "The_Invisible_Hand",
|
"Name_3": "The_Invisible_Hand",
|
||||||
"Name_4": "Nine_Billion_Names",
|
"Name_4": "Nine_Billion_Names",
|
||||||
"Duration": 20
|
"Duration": 20,
|
||||||
|
"Assignment": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -364,13 +364,23 @@
|
|||||||
"type": "stored",
|
"type": "stored",
|
||||||
"value": {},
|
"value": {},
|
||||||
"display": "hide",
|
"display": "hide",
|
||||||
"stored": "StoredTrailblazePower"
|
"stored": "StoredTrailblazePower",
|
||||||
|
"order": 1,
|
||||||
|
"color": "#eb8efe"
|
||||||
},
|
},
|
||||||
"DungeonDouble": {
|
"DungeonDouble": {
|
||||||
"type": "stored",
|
"type": "stored",
|
||||||
"value": {},
|
"value": {},
|
||||||
"display": "hide",
|
"display": "hide",
|
||||||
"stored": "StoredDungeonDouble"
|
"stored": "StoredDungeonDouble"
|
||||||
|
},
|
||||||
|
"SimulatedUniverse": {
|
||||||
|
"type": "stored",
|
||||||
|
"value": {},
|
||||||
|
"display": "hide",
|
||||||
|
"stored": "StoredSimulatedUniverse",
|
||||||
|
"order": 5,
|
||||||
|
"color": "#8fb5fe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -782,7 +792,9 @@
|
|||||||
"type": "stored",
|
"type": "stored",
|
||||||
"value": {},
|
"value": {},
|
||||||
"display": "hide",
|
"display": "hide",
|
||||||
"stored": "StoredDailyActivity"
|
"stored": "StoredDailyActivity",
|
||||||
|
"order": 2,
|
||||||
|
"color": "#ffcf70"
|
||||||
},
|
},
|
||||||
"DailyQuest": {
|
"DailyQuest": {
|
||||||
"type": "stored",
|
"type": "stored",
|
||||||
@ -931,6 +943,14 @@
|
|||||||
12,
|
12,
|
||||||
20
|
20
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"Assignment": {
|
||||||
|
"type": "stored",
|
||||||
|
"value": {},
|
||||||
|
"display": "hide",
|
||||||
|
"stored": "StoredAssignment",
|
||||||
|
"order": 3,
|
||||||
|
"color": "#deba95"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,8 +111,14 @@ DungeonSupport:
|
|||||||
DungeonStorage:
|
DungeonStorage:
|
||||||
TrailblazePower:
|
TrailblazePower:
|
||||||
stored: StoredTrailblazePower
|
stored: StoredTrailblazePower
|
||||||
|
order: 1
|
||||||
|
color: "#eb8efe"
|
||||||
DungeonDouble:
|
DungeonDouble:
|
||||||
stored: StoredDungeonDouble
|
stored: StoredDungeonDouble
|
||||||
|
SimulatedUniverse:
|
||||||
|
stored: StoredSimulatedUniverse
|
||||||
|
order: 5
|
||||||
|
color: "#8fb5fe"
|
||||||
|
|
||||||
AchievableQuest:
|
AchievableQuest:
|
||||||
# Quests will be injected in config updater
|
# Quests will be injected in config updater
|
||||||
@ -124,6 +130,8 @@ AchievableQuest:
|
|||||||
DailyStorage:
|
DailyStorage:
|
||||||
DailyActivity:
|
DailyActivity:
|
||||||
stored: StoredDailyActivity
|
stored: StoredDailyActivity
|
||||||
|
order: 2
|
||||||
|
color: "#ffcf70"
|
||||||
DailyQuest:
|
DailyQuest:
|
||||||
stored: StoredDaily
|
stored: StoredDaily
|
||||||
|
|
||||||
@ -143,4 +151,8 @@ Assignment:
|
|||||||
option: [ Nameless_Land_Nameless_People, ]
|
option: [ Nameless_Land_Nameless_People, ]
|
||||||
Duration:
|
Duration:
|
||||||
value: 20
|
value: 20
|
||||||
option: [ 4, 8, 12, 20 ]
|
option: [ 4, 8, 12, 20 ]
|
||||||
|
Assignment:
|
||||||
|
stored: StoredAssignment
|
||||||
|
order: 3
|
||||||
|
color: "#deba95"
|
||||||
|
@ -52,6 +52,16 @@ Overview:
|
|||||||
Waiting:
|
Waiting:
|
||||||
NoTask:
|
NoTask:
|
||||||
|
|
||||||
|
Dashboard:
|
||||||
|
# From lang.readable_time()
|
||||||
|
NoData:
|
||||||
|
TimeError:
|
||||||
|
JustNow:
|
||||||
|
MinutesAgo:
|
||||||
|
HoursAgo:
|
||||||
|
DaysAgo:
|
||||||
|
LongTimeAgo:
|
||||||
|
|
||||||
AddAlas:
|
AddAlas:
|
||||||
PopupTitle:
|
PopupTitle:
|
||||||
NewName:
|
NewName:
|
||||||
|
84
module/config/argument/stored.json
Normal file
84
module/config/argument/stored.json
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"TrailblazePower": {
|
||||||
|
"name": "TrailblazePower",
|
||||||
|
"path": "Dungeon.DungeonStorage.TrailblazePower",
|
||||||
|
"i18n": "DungeonStorage.TrailblazePower.name",
|
||||||
|
"stored": "StoredTrailblazePower",
|
||||||
|
"attrs": {
|
||||||
|
"time": "2020-01-01 00:00:00",
|
||||||
|
"total": 180,
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"order": 1,
|
||||||
|
"color": "#eb8efe"
|
||||||
|
},
|
||||||
|
"DailyActivity": {
|
||||||
|
"name": "DailyActivity",
|
||||||
|
"path": "DailyQuest.DailyStorage.DailyActivity",
|
||||||
|
"i18n": "DailyStorage.DailyActivity.name",
|
||||||
|
"stored": "StoredDailyActivity",
|
||||||
|
"attrs": {
|
||||||
|
"time": "2020-01-01 00:00:00",
|
||||||
|
"total": 500,
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"order": 2,
|
||||||
|
"color": "#ffcf70"
|
||||||
|
},
|
||||||
|
"Assignment": {
|
||||||
|
"name": "Assignment",
|
||||||
|
"path": "Assignment.Assignment.Assignment",
|
||||||
|
"i18n": "Assignment.Assignment.name",
|
||||||
|
"stored": "StoredAssignment",
|
||||||
|
"attrs": {
|
||||||
|
"time": "2020-01-01 00:00:00",
|
||||||
|
"total": 0,
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"order": 3,
|
||||||
|
"color": "#deba95"
|
||||||
|
},
|
||||||
|
"SimulatedUniverse": {
|
||||||
|
"name": "SimulatedUniverse",
|
||||||
|
"path": "Dungeon.DungeonStorage.SimulatedUniverse",
|
||||||
|
"i18n": "DungeonStorage.SimulatedUniverse.name",
|
||||||
|
"stored": "StoredSimulatedUniverse",
|
||||||
|
"attrs": {
|
||||||
|
"time": "2020-01-01 00:00:00",
|
||||||
|
"total": 0,
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"order": 5,
|
||||||
|
"color": "#8fb5fe"
|
||||||
|
},
|
||||||
|
"DungeonDouble": {
|
||||||
|
"name": "DungeonDouble",
|
||||||
|
"path": "Dungeon.DungeonStorage.DungeonDouble",
|
||||||
|
"i18n": "DungeonStorage.DungeonDouble.name",
|
||||||
|
"stored": "StoredDungeonDouble",
|
||||||
|
"attrs": {
|
||||||
|
"time": "2020-01-01 00:00:00",
|
||||||
|
"calyx": 0,
|
||||||
|
"relic": 0
|
||||||
|
},
|
||||||
|
"order": 0,
|
||||||
|
"color": "#777777"
|
||||||
|
},
|
||||||
|
"DailyQuest": {
|
||||||
|
"name": "DailyQuest",
|
||||||
|
"path": "DailyQuest.DailyStorage.DailyQuest",
|
||||||
|
"i18n": "DailyStorage.DailyQuest.name",
|
||||||
|
"stored": "StoredDaily",
|
||||||
|
"attrs": {
|
||||||
|
"time": "2020-01-01 00:00:00",
|
||||||
|
"quest1": "",
|
||||||
|
"quest2": "",
|
||||||
|
"quest3": "",
|
||||||
|
"quest4": "",
|
||||||
|
"quest5": "",
|
||||||
|
"quest6": ""
|
||||||
|
},
|
||||||
|
"order": 0,
|
||||||
|
"color": "#777777"
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,7 @@ class GeneratedConfig:
|
|||||||
# Group `DungeonStorage`
|
# Group `DungeonStorage`
|
||||||
DungeonStorage_TrailblazePower = {}
|
DungeonStorage_TrailblazePower = {}
|
||||||
DungeonStorage_DungeonDouble = {}
|
DungeonStorage_DungeonDouble = {}
|
||||||
|
DungeonStorage_SimulatedUniverse = {}
|
||||||
|
|
||||||
# Group `AchievableQuest`
|
# Group `AchievableQuest`
|
||||||
AchievableQuest_Complete_1_Daily_Mission = 'achievable' # achievable, not_set, not_supported
|
AchievableQuest_Complete_1_Daily_Mission = 'achievable' # achievable, not_set, not_supported
|
||||||
@ -95,3 +96,4 @@ class GeneratedConfig:
|
|||||||
Assignment_Name_3 = 'The_Invisible_Hand' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm
|
Assignment_Name_3 = 'The_Invisible_Hand' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm
|
||||||
Assignment_Name_4 = 'Nine_Billion_Names' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm
|
Assignment_Name_4 = 'Nine_Billion_Names' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm
|
||||||
Assignment_Duration = 20 # 4, 8, 12, 20
|
Assignment_Duration = 20 # 4, 8, 12, 20
|
||||||
|
Assignment_Assignment = {}
|
||||||
|
@ -441,6 +441,32 @@ class ConfigGenerator:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def stored(self):
|
||||||
|
import module.config.stored.classes as classes
|
||||||
|
data = {}
|
||||||
|
for path, value in deep_iter(self.args, depth=3):
|
||||||
|
if value.get('type') != 'stored':
|
||||||
|
continue
|
||||||
|
name = path[-1]
|
||||||
|
stored = value.get('stored')
|
||||||
|
stored_class = getattr(classes, stored)
|
||||||
|
row = {
|
||||||
|
'name': name,
|
||||||
|
'path': '.'.join(path),
|
||||||
|
'i18n': f'{path[1]}.{path[2]}.name',
|
||||||
|
'stored': stored,
|
||||||
|
'attrs': stored_class('')._attrs,
|
||||||
|
'order': value.get('order', 0),
|
||||||
|
'color': value.get('color', '#777777')
|
||||||
|
}
|
||||||
|
data[name] = row
|
||||||
|
|
||||||
|
# sort by `order` ascending, but `order`==0 at last
|
||||||
|
data = sorted(data.items(), key=lambda kv: (kv[1]['order'] == 0, kv[1]['order']))
|
||||||
|
data = {k: v for k, v in data}
|
||||||
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_deploy_template():
|
def generate_deploy_template():
|
||||||
template = poor_yaml_read(DEPLOY_TEMPLATE)
|
template = poor_yaml_read(DEPLOY_TEMPLATE)
|
||||||
@ -495,12 +521,14 @@ class ConfigGenerator:
|
|||||||
def generate(self):
|
def generate(self):
|
||||||
_ = self.args
|
_ = self.args
|
||||||
_ = self.menu
|
_ = self.menu
|
||||||
|
_ = self.stored
|
||||||
# _ = self.event
|
# _ = self.event
|
||||||
self.insert_assignment()
|
self.insert_assignment()
|
||||||
self.insert_package()
|
self.insert_package()
|
||||||
# self.insert_server()
|
# self.insert_server()
|
||||||
write_file(filepath_args(), self.args)
|
write_file(filepath_args(), self.args)
|
||||||
write_file(filepath_args('menu'), self.menu)
|
write_file(filepath_args('menu'), self.menu)
|
||||||
|
write_file(filepath_args('stored'), self.stored)
|
||||||
self.generate_code()
|
self.generate_code()
|
||||||
self.generate_stored()
|
self.generate_stored()
|
||||||
for lang in LANGUAGES:
|
for lang in LANGUAGES:
|
||||||
|
@ -364,6 +364,10 @@
|
|||||||
"DungeonDouble": {
|
"DungeonDouble": {
|
||||||
"name": "Dungeon Double",
|
"name": "Dungeon Double",
|
||||||
"help": ""
|
"help": ""
|
||||||
|
},
|
||||||
|
"SimulatedUniverse": {
|
||||||
|
"name": "Sim.Uni.",
|
||||||
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AchievableQuest": {
|
"AchievableQuest": {
|
||||||
@ -641,6 +645,10 @@
|
|||||||
"8": "8 Hours",
|
"8": "8 Hours",
|
||||||
"12": "12 Hours",
|
"12": "12 Hours",
|
||||||
"20": "20 Hours"
|
"20": "20 Hours"
|
||||||
|
},
|
||||||
|
"Assignment": {
|
||||||
|
"name": "Assignment",
|
||||||
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Gui": {
|
"Gui": {
|
||||||
@ -695,6 +703,15 @@
|
|||||||
"Waiting": "Waiting",
|
"Waiting": "Waiting",
|
||||||
"NoTask": "No Task"
|
"NoTask": "No Task"
|
||||||
},
|
},
|
||||||
|
"Dashboard": {
|
||||||
|
"NoData": "no data",
|
||||||
|
"TimeError": "time error",
|
||||||
|
"JustNow": "just now",
|
||||||
|
"MinutesAgo": "{time}min ago",
|
||||||
|
"HoursAgo": "{time}h ago",
|
||||||
|
"DaysAgo": "{time}d ago",
|
||||||
|
"LongTimeAgo": "long time ago"
|
||||||
|
},
|
||||||
"AddAlas": {
|
"AddAlas": {
|
||||||
"PopupTitle": "Add new config",
|
"PopupTitle": "Add new config",
|
||||||
"NewName": "New name",
|
"NewName": "New name",
|
||||||
|
@ -364,6 +364,10 @@
|
|||||||
"DungeonDouble": {
|
"DungeonDouble": {
|
||||||
"name": "DungeonStorage.DungeonDouble.name",
|
"name": "DungeonStorage.DungeonDouble.name",
|
||||||
"help": "DungeonStorage.DungeonDouble.help"
|
"help": "DungeonStorage.DungeonDouble.help"
|
||||||
|
},
|
||||||
|
"SimulatedUniverse": {
|
||||||
|
"name": "DungeonStorage.SimulatedUniverse.name",
|
||||||
|
"help": "DungeonStorage.SimulatedUniverse.help"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AchievableQuest": {
|
"AchievableQuest": {
|
||||||
@ -641,6 +645,10 @@
|
|||||||
"8": "8",
|
"8": "8",
|
||||||
"12": "12",
|
"12": "12",
|
||||||
"20": "20"
|
"20": "20"
|
||||||
|
},
|
||||||
|
"Assignment": {
|
||||||
|
"name": "Assignment.Assignment.name",
|
||||||
|
"help": "Assignment.Assignment.help"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Gui": {
|
"Gui": {
|
||||||
@ -695,6 +703,15 @@
|
|||||||
"Waiting": "Waiting",
|
"Waiting": "Waiting",
|
||||||
"NoTask": "No Task"
|
"NoTask": "No Task"
|
||||||
},
|
},
|
||||||
|
"Dashboard": {
|
||||||
|
"NoData": "Gui.Dashboard.NoData",
|
||||||
|
"TimeError": "Gui.Dashboard.TimeError",
|
||||||
|
"JustNow": "Gui.Dashboard.JustNow",
|
||||||
|
"MinutesAgo": "Gui.Dashboard.MinutesAgo",
|
||||||
|
"HoursAgo": "Gui.Dashboard.HoursAgo",
|
||||||
|
"DaysAgo": "Gui.Dashboard.DaysAgo",
|
||||||
|
"LongTimeAgo": "Gui.Dashboard.LongTimeAgo"
|
||||||
|
},
|
||||||
"AddAlas": {
|
"AddAlas": {
|
||||||
"PopupTitle": "新しいコンフィグを追加",
|
"PopupTitle": "新しいコンフィグを追加",
|
||||||
"NewName": "コンフィグ名",
|
"NewName": "コンフィグ名",
|
||||||
|
@ -364,6 +364,10 @@
|
|||||||
"DungeonDouble": {
|
"DungeonDouble": {
|
||||||
"name": "副本双倍",
|
"name": "副本双倍",
|
||||||
"help": ""
|
"help": ""
|
||||||
|
},
|
||||||
|
"SimulatedUniverse": {
|
||||||
|
"name": "模拟宇宙",
|
||||||
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AchievableQuest": {
|
"AchievableQuest": {
|
||||||
@ -641,6 +645,10 @@
|
|||||||
"8": "8小时",
|
"8": "8小时",
|
||||||
"12": "12小时",
|
"12": "12小时",
|
||||||
"20": "20小时"
|
"20": "20小时"
|
||||||
|
},
|
||||||
|
"Assignment": {
|
||||||
|
"name": "委托",
|
||||||
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Gui": {
|
"Gui": {
|
||||||
@ -695,6 +703,15 @@
|
|||||||
"Waiting": "等待中",
|
"Waiting": "等待中",
|
||||||
"NoTask": "无任务"
|
"NoTask": "无任务"
|
||||||
},
|
},
|
||||||
|
"Dashboard": {
|
||||||
|
"NoData": "无数据",
|
||||||
|
"TimeError": "时间错误",
|
||||||
|
"JustNow": "刚刚",
|
||||||
|
"MinutesAgo": "{time}分钟前",
|
||||||
|
"HoursAgo": "{time}小时前",
|
||||||
|
"DaysAgo": "{time}天前",
|
||||||
|
"LongTimeAgo": "很久以前"
|
||||||
|
},
|
||||||
"AddAlas": {
|
"AddAlas": {
|
||||||
"PopupTitle": "添加新配置",
|
"PopupTitle": "添加新配置",
|
||||||
"NewName": "新的配置文件名",
|
"NewName": "新的配置文件名",
|
||||||
|
@ -364,6 +364,10 @@
|
|||||||
"DungeonDouble": {
|
"DungeonDouble": {
|
||||||
"name": "副本雙倍",
|
"name": "副本雙倍",
|
||||||
"help": ""
|
"help": ""
|
||||||
|
},
|
||||||
|
"SimulatedUniverse": {
|
||||||
|
"name": "模擬宇宙",
|
||||||
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AchievableQuest": {
|
"AchievableQuest": {
|
||||||
@ -641,6 +645,10 @@
|
|||||||
"8": "8小時",
|
"8": "8小時",
|
||||||
"12": "12小時",
|
"12": "12小時",
|
||||||
"20": "20小時"
|
"20": "20小時"
|
||||||
|
},
|
||||||
|
"Assignment": {
|
||||||
|
"name": "委託",
|
||||||
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Gui": {
|
"Gui": {
|
||||||
@ -695,6 +703,15 @@
|
|||||||
"Waiting": "等待中",
|
"Waiting": "等待中",
|
||||||
"NoTask": "無任務"
|
"NoTask": "無任務"
|
||||||
},
|
},
|
||||||
|
"Dashboard": {
|
||||||
|
"NoData": "無數據",
|
||||||
|
"TimeError": "時間錯誤",
|
||||||
|
"JustNow": "剛剛",
|
||||||
|
"MinutesAgo": "{time}分鐘前",
|
||||||
|
"HoursAgo": "{time}小時前",
|
||||||
|
"DaysAgo": "{time}天前",
|
||||||
|
"LongTimeAgo": "很久以前"
|
||||||
|
},
|
||||||
"AddAlas": {
|
"AddAlas": {
|
||||||
"PopupTitle": "添加新的設定",
|
"PopupTitle": "添加新的設定",
|
||||||
"NewName": "新設定的檔案名",
|
"NewName": "新設定的檔案名",
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import time
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import cached_property as functools_cached_property
|
from functools import cached_property as functools_cached_property
|
||||||
|
|
||||||
@ -111,29 +110,6 @@ class StoredBase:
|
|||||||
from module.logger import logger
|
from module.logger import logger
|
||||||
logger.attr(self._name, self._stored)
|
logger.attr(self._name, self._stored)
|
||||||
|
|
||||||
def dashboard(self) -> str:
|
|
||||||
"""
|
|
||||||
Return a string to show on GUI
|
|
||||||
"""
|
|
||||||
return 'None'
|
|
||||||
|
|
||||||
def readable_time(self):
|
|
||||||
diff = self.time.timestamp() - time.time()
|
|
||||||
if diff < -1:
|
|
||||||
return '', 'TimeError'
|
|
||||||
elif diff < 60:
|
|
||||||
# < 1 min
|
|
||||||
return '', 'JustNow'
|
|
||||||
elif diff < 3600:
|
|
||||||
return str(int(diff // 60)), 'MinutesAgo'
|
|
||||||
elif diff < 86400:
|
|
||||||
return str(int(diff // 86400)), 'HoursAgo'
|
|
||||||
elif diff < 129600:
|
|
||||||
return str(int(diff // 129600)), 'DaysAgo'
|
|
||||||
else:
|
|
||||||
# > 15 days
|
|
||||||
return '', 'LongTimeAgo'
|
|
||||||
|
|
||||||
|
|
||||||
class StoredExpiredAt0400(StoredBase):
|
class StoredExpiredAt0400(StoredBase):
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
@ -154,11 +130,11 @@ class StoredCounter(StoredBase):
|
|||||||
|
|
||||||
FIXED_TOTAL = 0
|
FIXED_TOTAL = 0
|
||||||
|
|
||||||
def set(self, current, total=0):
|
def set(self, value, total=0):
|
||||||
if self.FIXED_TOTAL:
|
if self.FIXED_TOTAL:
|
||||||
total = self.FIXED_TOTAL
|
total = self.FIXED_TOTAL
|
||||||
with self._config.multi_set():
|
with self._config.multi_set():
|
||||||
self.value = current
|
self.value = value
|
||||||
self.total = total
|
self.total = total
|
||||||
|
|
||||||
def to_counter(self) -> str:
|
def to_counter(self) -> str:
|
||||||
@ -170,6 +146,13 @@ class StoredCounter(StoredBase):
|
|||||||
def get_remain(self) -> int:
|
def get_remain(self) -> int:
|
||||||
return self.total - self.value
|
return self.total - self.value
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _attrs(self) -> dict:
|
||||||
|
attrs = super()._attrs
|
||||||
|
if self.FIXED_TOTAL:
|
||||||
|
attrs['total'] = self.FIXED_TOTAL
|
||||||
|
return attrs
|
||||||
|
|
||||||
@functools_cached_property
|
@functools_cached_property
|
||||||
def _stored(self):
|
def _stored(self):
|
||||||
stored = super()._stored
|
stored = super()._stored
|
||||||
@ -186,6 +169,14 @@ class StoredTrailblazePower(StoredCounter):
|
|||||||
FIXED_TOTAL = 180
|
FIXED_TOTAL = 180
|
||||||
|
|
||||||
|
|
||||||
|
class StoredSimulatedUniverse(StoredCounter, StoredExpiredAt0400):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StoredAssignment(StoredCounter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StoredDaily(StoredExpiredAt0400):
|
class StoredDaily(StoredExpiredAt0400):
|
||||||
quest1 = ''
|
quest1 = ''
|
||||||
quest2 = ''
|
quest2 = ''
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from module.config.stored.classes import (
|
from module.config.stored.classes import (
|
||||||
|
StoredAssignment,
|
||||||
StoredBase,
|
StoredBase,
|
||||||
StoredCounter,
|
StoredCounter,
|
||||||
StoredDaily,
|
StoredDaily,
|
||||||
@ -6,6 +7,7 @@ from module.config.stored.classes import (
|
|||||||
StoredDungeonDouble,
|
StoredDungeonDouble,
|
||||||
StoredExpiredAt0400,
|
StoredExpiredAt0400,
|
||||||
StoredInt,
|
StoredInt,
|
||||||
|
StoredSimulatedUniverse,
|
||||||
StoredTrailblazePower,
|
StoredTrailblazePower,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,5 +18,7 @@ from module.config.stored.classes import (
|
|||||||
class StoredGenerated:
|
class StoredGenerated:
|
||||||
TrailblazePower = StoredTrailblazePower("Dungeon.DungeonStorage.TrailblazePower")
|
TrailblazePower = StoredTrailblazePower("Dungeon.DungeonStorage.TrailblazePower")
|
||||||
DungeonDouble = StoredDungeonDouble("Dungeon.DungeonStorage.DungeonDouble")
|
DungeonDouble = StoredDungeonDouble("Dungeon.DungeonStorage.DungeonDouble")
|
||||||
|
SimulatedUniverse = StoredSimulatedUniverse("Dungeon.DungeonStorage.SimulatedUniverse")
|
||||||
DailyActivity = StoredDailyActivity("DailyQuest.DailyStorage.DailyActivity")
|
DailyActivity = StoredDailyActivity("DailyQuest.DailyStorage.DailyActivity")
|
||||||
DailyQuest = StoredDaily("DailyQuest.DailyStorage.DailyQuest")
|
DailyQuest = StoredDaily("DailyQuest.DailyStorage.DailyQuest")
|
||||||
|
Assignment = StoredAssignment("Assignment.Assignment.Assignment")
|
||||||
|
@ -88,11 +88,13 @@ task_handler = TaskHandler()
|
|||||||
class AlasGUI(Frame):
|
class AlasGUI(Frame):
|
||||||
ALAS_MENU: Dict[str, Dict[str, List[str]]]
|
ALAS_MENU: Dict[str, Dict[str, List[str]]]
|
||||||
ALAS_ARGS: Dict[str, Dict[str, Dict[str, Dict[str, str]]]]
|
ALAS_ARGS: Dict[str, Dict[str, Dict[str, Dict[str, str]]]]
|
||||||
|
ALAS_STORED: Dict[str, Dict[str, Dict[str, str]]]
|
||||||
theme = "default"
|
theme = "default"
|
||||||
|
|
||||||
def initial(self) -> None:
|
def initial(self) -> None:
|
||||||
self.ALAS_MENU = read_file(filepath_args("menu", self.alas_mod))
|
self.ALAS_MENU = read_file(filepath_args("menu", self.alas_mod))
|
||||||
self.ALAS_ARGS = read_file(filepath_args("args", self.alas_mod))
|
self.ALAS_ARGS = read_file(filepath_args("args", self.alas_mod))
|
||||||
|
self.ALAS_STORED = read_file(filepath_args("stored", self.alas_mod))
|
||||||
self._init_alas_config_watcher()
|
self._init_alas_config_watcher()
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -318,6 +320,35 @@ class AlasGUI(Frame):
|
|||||||
color="navigator",
|
color="navigator",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_dashboard(self, arg, arg_dict, config):
|
||||||
|
i18n = arg_dict.get('i18n')
|
||||||
|
if i18n:
|
||||||
|
name = t(i18n)
|
||||||
|
else:
|
||||||
|
name = arg
|
||||||
|
color = arg_dict.get("color", "#777777")
|
||||||
|
nodata = t("Gui.Dashboard.NoData")
|
||||||
|
|
||||||
|
def set_value(dic):
|
||||||
|
if "total" in dic.get("attrs", []) and config.get("total") is not None:
|
||||||
|
return [
|
||||||
|
put_text(config.get("value", nodata)).style("--dashboard-value--"),
|
||||||
|
put_text(f' / {config.get("total", "")}').style("--dashboard-time--"),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
put_text(config.get("value", nodata)).style("--dashboard-value--"),
|
||||||
|
]
|
||||||
|
|
||||||
|
with use_scope(f"dashboard-row-{arg}", clear=True):
|
||||||
|
put_html(f'<div><div class="dashboard-icon" style="background-color:{color}"></div>'),
|
||||||
|
put_scope(f"dashboard-content-{arg}", [
|
||||||
|
put_scope(f"dashboard-value-{arg}", set_value(arg_dict)),
|
||||||
|
put_scope(f"dashboard-time-{arg}", [
|
||||||
|
put_text(f"{name} - {lang.readable_time(config.get('time', ''))}").style("--dashboard-time--"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
@use_scope("content", clear=True)
|
@use_scope("content", clear=True)
|
||||||
def alas_overview(self) -> None:
|
def alas_overview(self) -> None:
|
||||||
self.init_menu(name="Overview")
|
self.init_menu(name="Overview")
|
||||||
@ -374,20 +405,20 @@ class AlasGUI(Frame):
|
|||||||
log = RichLog("log")
|
log = RichLog("log")
|
||||||
|
|
||||||
with use_scope("logs"):
|
with use_scope("logs"):
|
||||||
put_scope(
|
put_scope("log-bar", [
|
||||||
"log-bar",
|
put_scope("log-title", [
|
||||||
[
|
put_text(t("Gui.Overview.Log")).style("font-size: 1.25rem; margin: auto .5rem auto;"),
|
||||||
put_text(t("Gui.Overview.Log")).style(
|
put_scope("log-title-btns", [
|
||||||
"font-size: 1.25rem; margin: auto .5rem auto;"
|
put_scope("log_scroll_btn"),
|
||||||
),
|
]),
|
||||||
put_scope(
|
]),
|
||||||
"log-bar-btns",
|
put_html('<hr class="hr-group">'),
|
||||||
[
|
put_scope("dashboard", [
|
||||||
put_scope("log_scroll_btn"),
|
# Empty dashboard, values will be updated in alas_update_overview_task()
|
||||||
],
|
put_scope(f"dashboard-row-{arg}", []) for arg in self.ALAS_STORED.keys()
|
||||||
),
|
# Empty content to left-align last row
|
||||||
],
|
] + [put_html("<i></i>")] * len(self.ALAS_STORED))
|
||||||
),
|
])
|
||||||
put_scope("log", [put_html("")])
|
put_scope("log", [put_html("")])
|
||||||
|
|
||||||
log.console.width = log.get_width()
|
log.console.width = log.get_width()
|
||||||
@ -501,6 +532,7 @@ class AlasGUI(Frame):
|
|||||||
self.alas_config.load()
|
self.alas_config.load()
|
||||||
self.alas_config.get_next_task()
|
self.alas_config.get_next_task()
|
||||||
|
|
||||||
|
alive = self.alas.alive
|
||||||
if len(self.alas_config.pending_task) >= 1:
|
if len(self.alas_config.pending_task) >= 1:
|
||||||
if self.alas.alive:
|
if self.alas.alive:
|
||||||
running = self.alas_config.pending_task[:1]
|
running = self.alas_config.pending_task[:1]
|
||||||
@ -528,27 +560,39 @@ class AlasGUI(Frame):
|
|||||||
color="off",
|
color="off",
|
||||||
)
|
)
|
||||||
|
|
||||||
clear("running_tasks")
|
if self.scope_expired("scheduler_alive", alive) \
|
||||||
clear("pending_tasks")
|
or self.scope_expired("pending_task", self.alas_config.pending_task):
|
||||||
clear("waiting_tasks")
|
clear("running_tasks")
|
||||||
with use_scope("running_tasks"):
|
clear("pending_tasks")
|
||||||
if running:
|
clear("waiting_tasks")
|
||||||
for task in running:
|
with use_scope("running_tasks"):
|
||||||
put_task(task)
|
if running:
|
||||||
else:
|
for task in running:
|
||||||
put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
|
put_task(task)
|
||||||
with use_scope("pending_tasks"):
|
else:
|
||||||
if pending:
|
put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
|
||||||
for task in pending:
|
with use_scope("pending_tasks"):
|
||||||
put_task(task)
|
if pending:
|
||||||
else:
|
for task in pending:
|
||||||
put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
|
put_task(task)
|
||||||
with use_scope("waiting_tasks"):
|
else:
|
||||||
if waiting:
|
put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
|
||||||
for task in waiting:
|
with use_scope("waiting_tasks"):
|
||||||
put_task(task)
|
if waiting:
|
||||||
else:
|
for task in waiting:
|
||||||
put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
|
put_task(task)
|
||||||
|
else:
|
||||||
|
put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
|
||||||
|
self.scope_add("scheduler_alive", alive)
|
||||||
|
self.scope_add("pending_task", self.alas_config.pending_task)
|
||||||
|
|
||||||
|
for arg, arg_dict in self.ALAS_STORED.items():
|
||||||
|
path = arg_dict["path"]
|
||||||
|
if self.scope_expired_then_add(
|
||||||
|
key=f"dashboard-time-value-{arg}",
|
||||||
|
value=lang.readable_time(deep_get(self.alas_config.data, keys=f"{path}.time"))
|
||||||
|
):
|
||||||
|
self.set_dashboard(arg, arg_dict, deep_get(self.alas_config.data, keys=path, default={}))
|
||||||
|
|
||||||
@use_scope("content", clear=True)
|
@use_scope("content", clear=True)
|
||||||
def alas_daemon_overview(self, task: str) -> None:
|
def alas_daemon_overview(self, task: str) -> None:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from pywebio.output import clear, put_html, put_scope, put_text, use_scope
|
from pywebio.output import clear, put_html, put_scope, put_text, use_scope
|
||||||
from pywebio.session import defer_call, info, run_js
|
from pywebio.session import defer_call, info, run_js
|
||||||
|
|
||||||
@ -13,12 +15,34 @@ class Base:
|
|||||||
self.is_mobile = info.user_agent.is_mobile
|
self.is_mobile = info.user_agent.is_mobile
|
||||||
# Task handler
|
# Task handler
|
||||||
self.task_handler = WebIOTaskHandler()
|
self.task_handler = WebIOTaskHandler()
|
||||||
|
# Record scopes to reduce data transfer to frontend
|
||||||
|
# Key: scope name, value: last update time
|
||||||
|
self.scope: Dict[str, Any] = {}
|
||||||
defer_call(self.stop)
|
defer_call(self.stop)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.task_handler.stop()
|
self.task_handler.stop()
|
||||||
|
|
||||||
|
def scope_clear(self):
|
||||||
|
self.scope = {}
|
||||||
|
|
||||||
|
def scope_add(self, key, value):
|
||||||
|
self.scope[key] = value
|
||||||
|
|
||||||
|
def scope_expired(self, key, value) -> bool:
|
||||||
|
try:
|
||||||
|
return self.scope[key] != value
|
||||||
|
except KeyError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def scope_expired_then_add(self, key, value) -> bool:
|
||||||
|
if self.scope_expired(key, value):
|
||||||
|
self.scope_add(key, value)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Frame(Base):
|
class Frame(Base):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -33,6 +57,7 @@ class Frame(Base):
|
|||||||
name: button name(label) to be highlight
|
name: button name(label) to be highlight
|
||||||
"""
|
"""
|
||||||
self.visible = True
|
self.visible = True
|
||||||
|
self.scope_clear()
|
||||||
self.task_handler.remove_pending_task()
|
self.task_handler.remove_pending_task()
|
||||||
clear("menu")
|
clear("menu")
|
||||||
if expand_menu:
|
if expand_menu:
|
||||||
@ -50,6 +75,7 @@ class Frame(Base):
|
|||||||
"""
|
"""
|
||||||
self.visible = True
|
self.visible = True
|
||||||
self.page = name
|
self.page = name
|
||||||
|
self.scope_clear()
|
||||||
self.task_handler.remove_pending_task()
|
self.task_handler.remove_pending_task()
|
||||||
clear("content")
|
clear("content")
|
||||||
if collapse_menu:
|
if collapse_menu:
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import time
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from module.config.utils import *
|
from module.config.utils import *
|
||||||
from module.webui.setting import State
|
|
||||||
from module.webui.fake import list_mod
|
from module.webui.fake import list_mod
|
||||||
|
from module.webui.setting import State
|
||||||
|
|
||||||
LANG = "zh-CN"
|
LANG = "zh-CN"
|
||||||
TRANSLATE_MODE = False
|
TRANSLATE_MODE = False
|
||||||
@ -67,3 +68,33 @@ def reload():
|
|||||||
for key in dic_lang["ja-JP"].keys():
|
for key in dic_lang["ja-JP"].keys():
|
||||||
if dic_lang["ja-JP"][key] == key:
|
if dic_lang["ja-JP"][key] == key:
|
||||||
dic_lang["ja-JP"][key] = dic_lang["en-US"][key]
|
dic_lang["ja-JP"][key] = dic_lang["en-US"][key]
|
||||||
|
|
||||||
|
|
||||||
|
def readable_time(before: str) -> str:
|
||||||
|
"""
|
||||||
|
Convert "2023-08-29 12:30:53" to "3 Minutes Ago"
|
||||||
|
"""
|
||||||
|
if not before:
|
||||||
|
return t("Gui.Dashboard.NoData")
|
||||||
|
try:
|
||||||
|
ti = datetime.fromisoformat(before)
|
||||||
|
except ValueError:
|
||||||
|
return t("Gui.Dashboard.TimeError")
|
||||||
|
if ti == DEFAULT_TIME:
|
||||||
|
return t("Gui.Dashboard.NoData")
|
||||||
|
|
||||||
|
diff = time.time() - ti.timestamp()
|
||||||
|
if diff < -1:
|
||||||
|
return t("Gui.Dashboard.TimeError")
|
||||||
|
elif diff < 60:
|
||||||
|
# < 1 min
|
||||||
|
return t("Gui.Dashboard.JustNow")
|
||||||
|
elif diff < 3600:
|
||||||
|
return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60))
|
||||||
|
elif diff < 86400:
|
||||||
|
return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600))
|
||||||
|
elif diff < 129600:
|
||||||
|
return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400))
|
||||||
|
else:
|
||||||
|
# > 15 days
|
||||||
|
return t("Gui.Dashboard.LongTimeAgo")
|
||||||
|
@ -145,7 +145,14 @@ class AssignmentUI(UI):
|
|||||||
@property
|
@property
|
||||||
def _limit_status(self) -> tuple[int, int, int]:
|
def _limit_status(self) -> tuple[int, int, int]:
|
||||||
self.device.screenshot()
|
self.device.screenshot()
|
||||||
return DigitCounter(OCR_ASSIGNMENT_LIMIT).ocr_single_line(self.device.image)
|
current, remain, total = DigitCounter(OCR_ASSIGNMENT_LIMIT).ocr_single_line(self.device.image)
|
||||||
|
if total and current <= total:
|
||||||
|
logger.attr('Assignment', f'{current}/{total}')
|
||||||
|
self.config.stored.Assignment.set(current, total)
|
||||||
|
else:
|
||||||
|
logger.warning(f'Invalid assignment limit: {current}/{total}')
|
||||||
|
self.config.stored.Assignment.set(0, 0)
|
||||||
|
return current, remain, total
|
||||||
|
|
||||||
def _iter_groups(self) -> Iterator[AssignmentGroup]:
|
def _iter_groups(self) -> Iterator[AssignmentGroup]:
|
||||||
for state in ASSIGNMENT_TOP_SWITCH.state_list:
|
for state in ASSIGNMENT_TOP_SWITCH.state_list:
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy import signal
|
from scipy import signal
|
||||||
|
from module.base.button import Button, ButtonWrapper
|
||||||
|
|
||||||
from module.base.timer import Timer
|
from module.base.timer import Timer
|
||||||
from module.base.utils import area_size, crop, rgb2luma, load_image
|
from module.base.utils import area_size, crop, rgb2luma, load_image, crop
|
||||||
from module.logger import logger
|
from module.logger import logger
|
||||||
from module.ui.scroll import Scroll
|
from module.ui.scroll import Scroll
|
||||||
|
from tasks.base.assets.assets_base_popup import CANCEL_POPUP
|
||||||
from tasks.base.ui import UI
|
from tasks.base.ui import UI
|
||||||
from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST, \
|
from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST, \
|
||||||
COMBAT_SUPPORT_LIST_SCROLL, COMBAT_SUPPORT_SELECTED
|
COMBAT_SUPPORT_LIST_SCROLL, COMBAT_SUPPORT_SELECTED, COMBAT_SUPPORT_LIST_GRID
|
||||||
from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_SUPPORT, COMBAT_TEAM_DISMISSSUPPORT
|
from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_SUPPORT, COMBAT_TEAM_DISMISSSUPPORT
|
||||||
|
|
||||||
|
|
||||||
|
def get_position_in_original_image(position_in_croped_image, crop_area):
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
tuple: (x, y) of position in original image
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
position_in_croped_image[0] + crop_area[0], position_in_croped_image[1] + crop_area[1]) if position_in_croped_image else None
|
||||||
|
|
||||||
|
|
||||||
class SupportCharacter:
|
class SupportCharacter:
|
||||||
_image_cache = {}
|
_image_cache = {}
|
||||||
|
_crop_area = COMBAT_SUPPORT_LIST_GRID.matched_button.area
|
||||||
|
|
||||||
def __init__(self, name, screenshot, similarity=0.85):
|
def __init__(self, name, screenshot, similarity=0.85):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.image = self._scale_character()
|
self.image = self._scale_character()
|
||||||
self.screenshot = screenshot
|
self.screenshot = crop(screenshot, SupportCharacter._crop_area)
|
||||||
self.similarity = similarity
|
self.similarity = similarity
|
||||||
self.button = self._find_character()
|
self.button = self._find_character()
|
||||||
|
|
||||||
@ -49,10 +62,12 @@ class SupportCharacter:
|
|||||||
def _find_character(self):
|
def _find_character(self):
|
||||||
character = np.array(self.image)
|
character = np.array(self.image)
|
||||||
support_list_img = self.screenshot
|
support_list_img = self.screenshot
|
||||||
res = cv2.matchTemplate(character, support_list_img, cv2.TM_CCOEFF_NORMED)
|
res = cv2.matchTemplate(
|
||||||
|
character, support_list_img, cv2.TM_CCOEFF_NORMED)
|
||||||
|
|
||||||
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
||||||
|
max_loc = get_position_in_original_image(
|
||||||
|
max_loc, SupportCharacter._crop_area)
|
||||||
character_width = character.shape[1]
|
character_width = character.shape[1]
|
||||||
character_height = character.shape[0]
|
character_height = character.shape[0]
|
||||||
|
|
||||||
@ -68,6 +83,66 @@ class SupportCharacter:
|
|||||||
self.button[0], self.button[1] - 5, self.button[0] + 30, self.button[1]) if self.button else None
|
self.button[0], self.button[1] - 5, self.button[0] + 30, self.button[1]) if self.button else None
|
||||||
|
|
||||||
|
|
||||||
|
class ArrowWrapper(ButtonWrapper):
|
||||||
|
|
||||||
|
def find_center(self, image):
|
||||||
|
res = cv2.matchTemplate(
|
||||||
|
self.matched_button.image, image, cv2.TM_CCOEFF_NORMED)
|
||||||
|
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
(max_loc[0] + self.matched_button.image.shape[1] / 2),
|
||||||
|
(max_loc[1] + self.matched_button.image.shape[0] / 2),
|
||||||
|
)
|
||||||
|
if max_val > 0.75
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NextSupportCharacter:
|
||||||
|
_arrow = ArrowWrapper(
|
||||||
|
name="NextSupportCharacterArrow",
|
||||||
|
share=Button(
|
||||||
|
file='./assets/support/selected_character_arrow.png',
|
||||||
|
area=None,
|
||||||
|
search=None,
|
||||||
|
color=None,
|
||||||
|
button=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_crop_area = (290, 115, 435, 634)
|
||||||
|
|
||||||
|
def __init__(self, screenshot):
|
||||||
|
self.name = "SupportCharacterArrow"
|
||||||
|
self.screenshot = crop(screenshot, NextSupportCharacter._crop_area)
|
||||||
|
self.arrow_center = self._find_center()
|
||||||
|
self.button = self._get_next_support_character_button()
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.button is not None
|
||||||
|
|
||||||
|
def _find_center(self):
|
||||||
|
center = NextSupportCharacter._arrow.find_center(self.screenshot)
|
||||||
|
center = get_position_in_original_image(
|
||||||
|
center, NextSupportCharacter._crop_area) if center else None
|
||||||
|
return center
|
||||||
|
|
||||||
|
def _get_next_support_character_button(self):
|
||||||
|
area = (self.arrow_center[0] - 200, min(self.arrow_center[1] + 65, 615), self.arrow_center[0] + 10, min(
|
||||||
|
self.arrow_center[1] + 80, 620)) if self.arrow_center and self.arrow_center[1] < 510 else None
|
||||||
|
return ButtonWrapper(
|
||||||
|
name="NextSupportCharacterButton",
|
||||||
|
share=Button(
|
||||||
|
file='./assets/support/selected_character_arrow.png',
|
||||||
|
area=area,
|
||||||
|
search=area,
|
||||||
|
# if next support was selected, the average color of the button will larger than 220
|
||||||
|
color=(220, 220, 220),
|
||||||
|
button=area,
|
||||||
|
)
|
||||||
|
) if self.arrow_center and self.arrow_center[1] < 510 else None
|
||||||
|
|
||||||
|
|
||||||
class SupportListScroll(Scroll):
|
class SupportListScroll(Scroll):
|
||||||
def cal_position(self, main):
|
def cal_position(self, main):
|
||||||
"""
|
"""
|
||||||
@ -130,11 +205,18 @@ class CombatSupport(UI):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Click
|
# Click
|
||||||
if self.appear(COMBAT_TEAM_SUPPORT, interval=2):
|
if self.appear(COMBAT_TEAM_SUPPORT, interval=1):
|
||||||
self.device.click(COMBAT_TEAM_SUPPORT)
|
self.device.click(COMBAT_TEAM_SUPPORT)
|
||||||
self.interval_reset(COMBAT_TEAM_SUPPORT)
|
self.interval_reset(COMBAT_TEAM_SUPPORT)
|
||||||
continue
|
continue
|
||||||
if self.appear(COMBAT_SUPPORT_LIST, interval=2):
|
if self.appear(CANCEL_POPUP, interval=1):
|
||||||
|
logger.warning(
|
||||||
|
"selected identical character, trying select another")
|
||||||
|
self._cancel_popup()
|
||||||
|
self._select_next_support()
|
||||||
|
self.interval_reset(CANCEL_POPUP)
|
||||||
|
continue
|
||||||
|
if self.appear(COMBAT_SUPPORT_LIST, interval=1):
|
||||||
if support_character_name != "FirstCharacter":
|
if support_character_name != "FirstCharacter":
|
||||||
self._search_support(
|
self._search_support(
|
||||||
support_character_name) # Search support
|
support_character_name) # Search support
|
||||||
@ -167,7 +249,6 @@ class CombatSupport(UI):
|
|||||||
|
|
||||||
logger.info("Searching support")
|
logger.info("Searching support")
|
||||||
skip_first_screenshot = False
|
skip_first_screenshot = False
|
||||||
character = None
|
|
||||||
while 1:
|
while 1:
|
||||||
if skip_first_screenshot:
|
if skip_first_screenshot:
|
||||||
skip_first_screenshot = False
|
skip_first_screenshot = False
|
||||||
@ -175,7 +256,8 @@ class CombatSupport(UI):
|
|||||||
self.device.screenshot()
|
self.device.screenshot()
|
||||||
|
|
||||||
if not support_character_name.startswith("Trailblazer"):
|
if not support_character_name.startswith("Trailblazer"):
|
||||||
character = SupportCharacter(support_character_name, self.device.image)
|
character = SupportCharacter(
|
||||||
|
support_character_name, self.device.image)
|
||||||
else:
|
else:
|
||||||
character = SupportCharacter(f"Stelle{support_character_name[11:]}",
|
character = SupportCharacter(f"Stelle{support_character_name[11:]}",
|
||||||
self.device.image) or SupportCharacter(
|
self.device.image) or SupportCharacter(
|
||||||
@ -223,3 +305,73 @@ class CombatSupport(UI):
|
|||||||
self.device.click(character)
|
self.device.click(character)
|
||||||
interval.reset()
|
interval.reset()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
def _cancel_popup(self):
|
||||||
|
"""
|
||||||
|
Pages:
|
||||||
|
in: CANCEL_POPUP
|
||||||
|
out: COMBAT_SUPPORT_LIST
|
||||||
|
"""
|
||||||
|
logger.hr("Combat support cancel popup")
|
||||||
|
skip_first_screenshot = True
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
if skip_first_screenshot:
|
||||||
|
skip_first_screenshot = False
|
||||||
|
else:
|
||||||
|
self.device.screenshot()
|
||||||
|
|
||||||
|
# End
|
||||||
|
if self.appear(COMBAT_SUPPORT_LIST):
|
||||||
|
logger.info("Popup canceled")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.appear(CANCEL_POPUP):
|
||||||
|
self.device.click(CANCEL_POPUP)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def _select_next_support(self):
|
||||||
|
"""
|
||||||
|
Pages:
|
||||||
|
in: COMBAT_SUPPORT_LIST
|
||||||
|
out: COMBAT_SUPPORT_LIST
|
||||||
|
"""
|
||||||
|
skip_first_screenshot = True
|
||||||
|
scroll = SupportListScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, color=(194, 196, 205),
|
||||||
|
name=COMBAT_SUPPORT_LIST_SCROLL.name)
|
||||||
|
interval = Timer(1)
|
||||||
|
next_support = None
|
||||||
|
if scroll.appear(main=self):
|
||||||
|
while 1:
|
||||||
|
if skip_first_screenshot:
|
||||||
|
skip_first_screenshot = False
|
||||||
|
else:
|
||||||
|
self.device.screenshot()
|
||||||
|
|
||||||
|
# End
|
||||||
|
if next_support and self._next_support_selected(next_support):
|
||||||
|
return
|
||||||
|
|
||||||
|
if interval.reached():
|
||||||
|
next_support = NextSupportCharacter(self.device.image)
|
||||||
|
if next_support:
|
||||||
|
logger.info("Next support found, clicking")
|
||||||
|
self.device.click(next_support.button)
|
||||||
|
elif not scroll.at_bottom(main=self):
|
||||||
|
scroll.next_page(main=self, page=0.4)
|
||||||
|
else:
|
||||||
|
logger.warning("No more support")
|
||||||
|
return
|
||||||
|
|
||||||
|
interval.reset()
|
||||||
|
continue
|
||||||
|
|
||||||
|
def _next_support_selected(self, next_support: NextSupportCharacter):
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
bool: True if selected else False
|
||||||
|
"""
|
||||||
|
if self.match_color(next_support.button, threshold=20):
|
||||||
|
logger.info("Next support selected")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -53,6 +53,16 @@ OCR_DUNGEON_NAV = ButtonWrapper(
|
|||||||
button=(108, 132, 428, 613),
|
button=(108, 132, 428, 613),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
OCR_SIMUNI_POINT = ButtonWrapper(
|
||||||
|
name='OCR_SIMUNI_POINT',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/dungeon/ui/OCR_SIMUNI_POINT.png',
|
||||||
|
area=(580, 190, 880, 235),
|
||||||
|
search=(560, 170, 900, 255),
|
||||||
|
color=(158, 164, 252),
|
||||||
|
button=(580, 190, 880, 235),
|
||||||
|
),
|
||||||
|
)
|
||||||
OPERATION_BRIEFING_CHECK = ButtonWrapper(
|
OPERATION_BRIEFING_CHECK = ButtonWrapper(
|
||||||
name='OPERATION_BRIEFING_CHECK',
|
name='OPERATION_BRIEFING_CHECK',
|
||||||
share=Button(
|
share=Button(
|
||||||
|
@ -143,6 +143,9 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
|
|||||||
with self.config.multi_set():
|
with self.config.multi_set():
|
||||||
self.config.stored.DungeonDouble.calyx = calyx
|
self.config.stored.DungeonDouble.calyx = calyx
|
||||||
self.config.stored.DungeonDouble.relic = relic
|
self.config.stored.DungeonDouble.relic = relic
|
||||||
|
# Update SimulatedUniverse points
|
||||||
|
logger.info('Get simulated universe points')
|
||||||
|
self.dungeon_get_simuni_point()
|
||||||
|
|
||||||
# Run double events
|
# Run double events
|
||||||
ran_calyx_golden = False
|
ran_calyx_golden = False
|
||||||
|
@ -5,7 +5,7 @@ from module.base.button import ClickButton
|
|||||||
from module.base.timer import Timer
|
from module.base.timer import Timer
|
||||||
from module.base.utils import get_color
|
from module.base.utils import get_color
|
||||||
from module.logger import logger
|
from module.logger import logger
|
||||||
from module.ocr.ocr import Ocr, OcrResultButton
|
from module.ocr.ocr import DigitCounter, Ocr, OcrResultButton
|
||||||
from module.ocr.utils import split_and_pair_button_attr
|
from module.ocr.utils import split_and_pair_button_attr
|
||||||
from module.ui.draggable_list import DraggableList
|
from module.ui.draggable_list import DraggableList
|
||||||
from module.ui.switch import Switch
|
from module.ui.switch import Switch
|
||||||
@ -74,6 +74,10 @@ class OcrDungeonList(Ocr):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class OcrSimUniPoint(DigitCounter):
|
||||||
|
merge_thres_x = 50
|
||||||
|
|
||||||
|
|
||||||
class OcrDungeonListLimitEntrance(OcrDungeonList):
|
class OcrDungeonListLimitEntrance(OcrDungeonList):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -219,6 +223,22 @@ class DungeonUI(UI):
|
|||||||
logger.info('DungeonNav row Forgotten_Hall stabled')
|
logger.info('DungeonNav row Forgotten_Hall stabled')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def dungeon_get_simuni_point(self) -> int:
|
||||||
|
"""
|
||||||
|
Page:
|
||||||
|
in: page_guide, Survival_Index, Simulated_Universe
|
||||||
|
"""
|
||||||
|
ocr = OcrSimUniPoint(OCR_SIMUNI_POINT)
|
||||||
|
value, _, total = ocr.ocr_single_line(self.device.image)
|
||||||
|
if total and value <= total:
|
||||||
|
logger.attr('SimulatedUniverse', f'{value}/{total}')
|
||||||
|
self.config.stored.SimulatedUniverse.set(value, total)
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}')
|
||||||
|
self.config.stored.SimulatedUniverse.set(0, 0)
|
||||||
|
return 0
|
||||||
|
|
||||||
def _dungeon_nav_goto(self, dungeon: DungeonList, skip_first_screenshot=True):
|
def _dungeon_nav_goto(self, dungeon: DungeonList, skip_first_screenshot=True):
|
||||||
"""
|
"""
|
||||||
Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)`
|
Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)`
|
||||||
|
Loading…
Reference in New Issue
Block a user